Keine Beschreibung

WMPageController.m 33KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864
  1. //
  2. // WMPageController.m
  3. // WMPageController
  4. //
  5. // Created by Mark on 15/6/11.
  6. // Copyright (c) 2015年 yq. All rights reserved.
  7. //
  8. #import "WMPageController.h"
  9. NSString *const WMControllerDidAddToSuperViewNotification = @"WMControllerDidAddToSuperViewNotification";
  10. NSString *const WMControllerDidFullyDisplayedNotification = @"WMControllerDidFullyDisplayedNotification";
  11. static NSInteger const kWMUndefinedIndex = -1;
  12. static NSInteger const kWMControllerCountUndefined = -1;
  13. @interface WMPageController () {
  14. CGFloat _targetX;
  15. CGRect _contentViewFrame, _menuViewFrame;
  16. BOOL _hasInited, _shouldNotScroll;
  17. NSInteger _initializedIndex, _controllerCount, _markedSelectIndex;
  18. }
  19. @property (nonatomic, strong, readwrite) UIViewController *currentViewController;
  20. // 用于记录子控制器view的frame,用于 scrollView 上的展示的位置
  21. @property (nonatomic, strong) NSMutableArray *childViewFrames;
  22. // 当前展示在屏幕上的控制器,方便在滚动的时候读取 (避免不必要计算)
  23. @property (nonatomic, strong) NSMutableDictionary *displayVC;
  24. // 用于记录销毁的viewController的位置 (如果它是某一种scrollView的Controller的话)
  25. @property (nonatomic, strong) NSMutableDictionary *posRecords;
  26. // 用于缓存加载过的控制器
  27. @property (nonatomic, strong) NSCache *memCache;
  28. @property (nonatomic, strong) NSMutableDictionary *backgroundCache;
  29. // 收到内存警告的次数
  30. @property (nonatomic, assign) int memoryWarningCount;
  31. @property (nonatomic, readonly) NSInteger childControllersCount;
  32. @end
  33. @implementation WMPageController
  34. #pragma mark - Lazy Loading
  35. - (NSMutableDictionary *)posRecords {
  36. if (_posRecords == nil) {
  37. _posRecords = [[NSMutableDictionary alloc] init];
  38. }
  39. return _posRecords;
  40. }
  41. - (NSMutableDictionary *)displayVC {
  42. if (_displayVC == nil) {
  43. _displayVC = [[NSMutableDictionary alloc] init];
  44. }
  45. return _displayVC;
  46. }
  47. - (NSMutableDictionary *)backgroundCache {
  48. if (_backgroundCache == nil) {
  49. _backgroundCache = [[NSMutableDictionary alloc] init];
  50. }
  51. return _backgroundCache;
  52. }
  53. #pragma mark - Public Methods
  54. - (instancetype)initWithCoder:(NSCoder *)aDecoder {
  55. if (self = [super initWithCoder:aDecoder]) {
  56. [self wm_setup];
  57. }
  58. return self;
  59. }
  60. - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
  61. if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) {
  62. [self wm_setup];
  63. }
  64. return self;
  65. }
  66. - (instancetype)initWithViewControllerClasses:(NSArray<Class> *)classes andTheirTitles:(NSArray<NSString *> *)titles {
  67. if (self = [self initWithNibName:nil bundle:nil]) {
  68. NSParameterAssert(classes.count == titles.count);
  69. _viewControllerClasses = [NSArray arrayWithArray:classes];
  70. _titles = [NSArray arrayWithArray:titles];
  71. }
  72. return self;
  73. }
  74. - (void)dealloc {
  75. [[NSNotificationCenter defaultCenter] removeObserver:self];
  76. [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(wm_growCachePolicyAfterMemoryWarning) object:nil];
  77. [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(wm_growCachePolicyToHigh) object:nil];
  78. }
  79. - (void)forceLayoutSubviews {
  80. if (!self.childControllersCount) return;
  81. // 计算宽高及子控制器的视图frame
  82. [self wm_calculateSize];
  83. [self wm_adjustScrollViewFrame];
  84. [self wm_adjustMenuViewFrame];
  85. [self wm_adjustDisplayingViewControllersFrame];
  86. }
  87. - (void)setScrollEnable:(BOOL)scrollEnable {
  88. _scrollEnable = scrollEnable;
  89. if (!self.scrollView) return;
  90. self.scrollView.scrollEnabled = scrollEnable;
  91. }
  92. - (void)setProgressViewCornerRadius:(CGFloat)progressViewCornerRadius {
  93. _progressViewCornerRadius = progressViewCornerRadius;
  94. if (self.menuView) {
  95. self.menuView.progressViewCornerRadius = progressViewCornerRadius;
  96. }
  97. }
  98. - (void)setMenuViewLayoutMode:(WMMenuViewLayoutMode)menuViewLayoutMode {
  99. _menuViewLayoutMode = menuViewLayoutMode;
  100. if (self.menuView.superview) {
  101. [self wm_resetMenuView];
  102. }
  103. }
  104. - (void)setCachePolicy:(WMPageControllerCachePolicy)cachePolicy {
  105. _cachePolicy = cachePolicy;
  106. if (cachePolicy != WMPageControllerCachePolicyDisabled) {
  107. self.memCache.countLimit = _cachePolicy;
  108. }
  109. }
  110. - (void)setSelectIndex:(int)selectIndex {
  111. _selectIndex = selectIndex;
  112. _markedSelectIndex = kWMUndefinedIndex;
  113. if (self.menuView && _hasInited) {
  114. [self.menuView selectItemAtIndex:selectIndex];
  115. } else {
  116. _markedSelectIndex = selectIndex;
  117. UIViewController *vc = [self.memCache objectForKey:@(selectIndex)];
  118. if (!vc) {
  119. vc = [self initializeViewControllerAtIndex:selectIndex];
  120. [self.memCache setObject:vc forKey:@(selectIndex)];
  121. }
  122. self.currentViewController = vc;
  123. }
  124. }
  125. - (void)setProgressViewIsNaughty:(BOOL)progressViewIsNaughty {
  126. _progressViewIsNaughty = progressViewIsNaughty;
  127. if (self.menuView) {
  128. self.menuView.progressViewIsNaughty = progressViewIsNaughty;
  129. }
  130. }
  131. - (void)setProgressWidth:(CGFloat)progressWidth {
  132. _progressWidth = progressWidth;
  133. self.progressViewWidths = ({
  134. NSMutableArray *tmp = [NSMutableArray array];
  135. for (int i = 0; i < self.childControllersCount; i++) {
  136. [tmp addObject:@(progressWidth)];
  137. }
  138. tmp.copy;
  139. });
  140. }
  141. - (void)setProgressViewWidths:(NSArray *)progressViewWidths {
  142. _progressViewWidths = progressViewWidths;
  143. if (self.menuView) {
  144. self.menuView.progressWidths = progressViewWidths;
  145. }
  146. }
  147. - (void)setMenuViewContentMargin:(CGFloat)menuViewContentMargin {
  148. _menuViewContentMargin = menuViewContentMargin;
  149. if (self.menuView) {
  150. self.menuView.contentMargin = menuViewContentMargin;
  151. }
  152. }
  153. - (void)reloadData {
  154. [self wm_clearDatas];
  155. if (!self.childControllersCount) return;
  156. [self wm_resetScrollView];
  157. [self.memCache removeAllObjects];
  158. [self wm_resetMenuView];
  159. [self viewDidLayoutSubviews];
  160. [self didEnterController:self.currentViewController atIndex:self.selectIndex];
  161. }
  162. - (void)updateTitle:(NSString *)title atIndex:(NSInteger)index {
  163. [self.menuView updateTitle:title atIndex:index andWidth:NO];
  164. }
  165. - (void)updateAttributeTitle:(NSAttributedString * _Nonnull)title atIndex:(NSInteger)index {
  166. [self.menuView updateAttributeTitle:title atIndex:index andWidth:NO];
  167. }
  168. - (void)updateTitle:(NSString *)title andWidth:(CGFloat)width atIndex:(NSInteger)index {
  169. if (self.itemsWidths && index < self.itemsWidths.count) {
  170. NSMutableArray *mutableWidths = [NSMutableArray arrayWithArray:self.itemsWidths];
  171. mutableWidths[index] = @(width);
  172. self.itemsWidths = [mutableWidths copy];
  173. } else {
  174. NSMutableArray *mutableWidths = [NSMutableArray array];
  175. for (int i = 0; i < self.childControllersCount; i++) {
  176. CGFloat itemWidth = (i == index) ? width : self.menuItemWidth;
  177. [mutableWidths addObject:@(itemWidth)];
  178. }
  179. self.itemsWidths = [mutableWidths copy];
  180. }
  181. [self.menuView updateTitle:title atIndex:index andWidth:YES];
  182. }
  183. - (void)setShowOnNavigationBar:(BOOL)showOnNavigationBar {
  184. if (_showOnNavigationBar == showOnNavigationBar) {
  185. return;
  186. }
  187. _showOnNavigationBar = showOnNavigationBar;
  188. if (self.menuView) {
  189. [self.menuView removeFromSuperview];
  190. [self wm_addMenuView];
  191. [self forceLayoutSubviews];
  192. [self.menuView slideMenuAtProgress:self.selectIndex];
  193. }
  194. }
  195. #pragma mark - Notification
  196. - (void)willResignActive:(NSNotification *)notification {
  197. for (int i = 0; i < self.childControllersCount; i++) {
  198. id obj = [self.memCache objectForKey:@(i)];
  199. if (obj) {
  200. [self.backgroundCache setObject:obj forKey:@(i)];
  201. }
  202. }
  203. }
  204. - (void)willEnterForeground:(NSNotification *)notification {
  205. for (NSNumber *key in self.backgroundCache.allKeys) {
  206. if (![self.memCache objectForKey:key]) {
  207. [self.memCache setObject:self.backgroundCache[key] forKey:key];
  208. }
  209. }
  210. [self.backgroundCache removeAllObjects];
  211. }
  212. #pragma mark - Delegate
  213. - (NSDictionary *)infoWithIndex:(NSInteger)index {
  214. NSString *title = [self titleAtIndex:index];
  215. return @{@"title": title ?: @"", @"index": @(index)};
  216. }
  217. - (void)willCachedController:(UIViewController *)vc atIndex:(NSInteger)index {
  218. if (self.childControllersCount && [self.delegate respondsToSelector:@selector(pageController:willCachedViewController:withInfo:)]) {
  219. NSDictionary *info = [self infoWithIndex:index];
  220. [self.delegate pageController:self willCachedViewController:vc withInfo:info];
  221. }
  222. }
  223. - (void)willEnterController:(UIViewController *)vc atIndex:(NSInteger)index {
  224. _selectIndex = (int)index;
  225. if (self.childControllersCount && [self.delegate respondsToSelector:@selector(pageController:willEnterViewController:withInfo:)]) {
  226. NSDictionary *info = [self infoWithIndex:index];
  227. [self.delegate pageController:self willEnterViewController:vc withInfo:info];
  228. }
  229. }
  230. // 完全进入控制器 (即停止滑动后调用)
  231. - (void)didEnterController:(UIViewController *)vc atIndex:(NSInteger)index {
  232. if (!self.childControllersCount) return;
  233. // Post FullyDisplayedNotification
  234. [self wm_postFullyDisplayedNotificationWithCurrentIndex:self.selectIndex];
  235. NSDictionary *info = [self infoWithIndex:index];
  236. if ([self.delegate respondsToSelector:@selector(pageController:didEnterViewController:withInfo:)]) {
  237. [self.delegate pageController:self didEnterViewController:vc withInfo:info];
  238. }
  239. // 当控制器创建时,调用延迟加载的代理方法
  240. if (_initializedIndex == index && [self.delegate respondsToSelector:@selector(pageController:lazyLoadViewController:withInfo:)]) {
  241. [self.delegate pageController:self lazyLoadViewController:vc withInfo:info];
  242. _initializedIndex = kWMUndefinedIndex;
  243. }
  244. // 根据 preloadPolicy 预加载控制器
  245. if (self.preloadPolicy == WMPageControllerPreloadPolicyNever) return;
  246. int length = (int)self.preloadPolicy;
  247. int start = 0;
  248. int end = (int)self.childControllersCount - 1;
  249. if (index > length) {
  250. start = (int)index - length;
  251. }
  252. if (self.childControllersCount - 1 > length + index) {
  253. end = (int)index + length;
  254. }
  255. for (int i = start; i <= end; i++) {
  256. // 如果已存在,不需要预加载
  257. if (![self.memCache objectForKey:@(i)] && !self.displayVC[@(i)]) {
  258. [self wm_addViewControllerAtIndex:i];
  259. [self wm_postAddToSuperViewNotificationWithIndex:i];
  260. }
  261. }
  262. _selectIndex = (int)index;
  263. }
  264. #pragma mark - Data source
  265. - (NSInteger)childControllersCount {
  266. if (_controllerCount == kWMControllerCountUndefined) {
  267. if ([self.dataSource respondsToSelector:@selector(numbersOfChildControllersInPageController:)]) {
  268. _controllerCount = [self.dataSource numbersOfChildControllersInPageController:self];
  269. } else {
  270. _controllerCount = self.viewControllerClasses.count;
  271. }
  272. }
  273. return _controllerCount;
  274. }
  275. - (UIViewController * _Nonnull)initializeViewControllerAtIndex:(NSInteger)index {
  276. if ([self.dataSource respondsToSelector:@selector(pageController:viewControllerAtIndex:)]) {
  277. return [self.dataSource pageController:self viewControllerAtIndex:index];
  278. }
  279. return [[self.viewControllerClasses[index] alloc] init];
  280. }
  281. - (NSString * _Nonnull)titleAtIndex:(NSInteger)index {
  282. NSString *title = nil;
  283. if ([self.dataSource respondsToSelector:@selector(pageController:titleAtIndex:)]) {
  284. title = [self.dataSource pageController:self titleAtIndex:index];
  285. } else {
  286. title = self.titles[index];
  287. }
  288. return (title ?: @"");
  289. }
  290. #pragma mark - Private Methods
  291. - (void)wm_resetScrollView {
  292. if (self.scrollView) {
  293. [self.scrollView removeFromSuperview];
  294. }
  295. [self wm_addScrollView];
  296. [self wm_addViewControllerAtIndex:self.selectIndex];
  297. self.currentViewController = self.displayVC[@(self.selectIndex)];
  298. }
  299. - (void)wm_clearDatas {
  300. _controllerCount = kWMControllerCountUndefined;
  301. _hasInited = NO;
  302. NSUInteger maxIndex = (self.childControllersCount - 1 > 0) ? (self.childControllersCount - 1) : 0;
  303. _selectIndex = self.selectIndex < self.childControllersCount ? self.selectIndex : (int)maxIndex;
  304. if (self.progressWidth > 0) { self.progressWidth = self.progressWidth; }
  305. NSArray *displayingViewControllers = self.displayVC.allValues;
  306. for (UIViewController *vc in displayingViewControllers) {
  307. [vc.view removeFromSuperview];
  308. [vc willMoveToParentViewController:nil];
  309. [vc removeFromParentViewController];
  310. }
  311. self.memoryWarningCount = 0;
  312. [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(wm_growCachePolicyAfterMemoryWarning) object:nil];
  313. [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(wm_growCachePolicyToHigh) object:nil];
  314. self.currentViewController = nil;
  315. [self.posRecords removeAllObjects];
  316. [self.displayVC removeAllObjects];
  317. }
  318. // 当子控制器init完成时发送通知
  319. - (void)wm_postAddToSuperViewNotificationWithIndex:(int)index {
  320. if (!self.postNotification) return;
  321. NSDictionary *info = @{
  322. @"index":@(index),
  323. @"title":[self titleAtIndex:index]
  324. };
  325. [[NSNotificationCenter defaultCenter] postNotificationName:WMControllerDidAddToSuperViewNotification
  326. object:self
  327. userInfo:info];
  328. }
  329. // 当子控制器完全展示在user面前时发送通知
  330. - (void)wm_postFullyDisplayedNotificationWithCurrentIndex:(int)index {
  331. if (!self.postNotification) return;
  332. NSDictionary *info = @{
  333. @"index":@(index),
  334. @"title":[self titleAtIndex:index]
  335. };
  336. [[NSNotificationCenter defaultCenter] postNotificationName:WMControllerDidFullyDisplayedNotification
  337. object:self
  338. userInfo:info];
  339. }
  340. // 初始化一些参数,在init中调用
  341. - (void)wm_setup {
  342. _titleSizeSelected = 18.0f;
  343. _titleSizeNormal = 15.0f;
  344. _titleColorSelected = [UIColor colorWithRed:168.0/255.0 green:20.0/255.0 blue:4/255.0 alpha:1];
  345. _titleColorNormal = [UIColor colorWithRed:0 green:0 blue:0 alpha:1];
  346. _menuItemWidth = 65.0f;
  347. _memCache = [[NSCache alloc] init];
  348. _initializedIndex = kWMUndefinedIndex;
  349. _markedSelectIndex = kWMUndefinedIndex;
  350. _controllerCount = kWMControllerCountUndefined;
  351. _scrollEnable = YES;
  352. _progressViewCornerRadius = WMUNDEFINED_VALUE;
  353. _progressHeight = WMUNDEFINED_VALUE;
  354. self.automaticallyCalculatesItemWidths = NO;
  355. self.automaticallyAdjustsScrollViewInsets = NO;
  356. self.preloadPolicy = WMPageControllerPreloadPolicyNever;
  357. self.cachePolicy = WMPageControllerCachePolicyNoLimit;
  358. self.delegate = self;
  359. self.dataSource = self;
  360. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(willResignActive:) name:UIApplicationWillResignActiveNotification object:nil];
  361. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(willEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil];
  362. }
  363. // 包括宽高,子控制器视图 frame
  364. - (void)wm_calculateSize {
  365. _menuViewFrame = [self.dataSource pageController:self preferredFrameForMenuView:self.menuView];
  366. _contentViewFrame = [self.dataSource pageController:self preferredFrameForContentView:self.scrollView];
  367. _childViewFrames = [NSMutableArray array];
  368. for (int i = 0; i < self.childControllersCount; i++) {
  369. CGRect frame = CGRectMake(i * _contentViewFrame.size.width, 0, _contentViewFrame.size.width, _contentViewFrame.size.height);
  370. [_childViewFrames addObject:[NSValue valueWithCGRect:frame]];
  371. }
  372. }
  373. - (void)wm_addScrollView {
  374. WMScrollView *scrollView = [[WMScrollView alloc] init];
  375. scrollView.scrollsToTop = NO;
  376. scrollView.pagingEnabled = YES;
  377. scrollView.backgroundColor = [UIColor whiteColor];
  378. scrollView.delegate = self;
  379. scrollView.showsVerticalScrollIndicator = NO;
  380. scrollView.showsHorizontalScrollIndicator = NO;
  381. scrollView.bounces = self.bounces;
  382. scrollView.scrollEnabled = self.scrollEnable;
  383. if (@available(iOS 11.0, *)) {
  384. scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
  385. }
  386. [self.view addSubview:scrollView];
  387. self.scrollView = scrollView;
  388. if (!self.navigationController) return;
  389. for (UIGestureRecognizer *gestureRecognizer in scrollView.gestureRecognizers) {
  390. [gestureRecognizer requireGestureRecognizerToFail:self.navigationController.interactivePopGestureRecognizer];
  391. }
  392. }
  393. - (void)wm_addMenuView {
  394. WMMenuView *menuView = [[WMMenuView alloc] initWithFrame:CGRectZero];
  395. menuView.delegate = self;
  396. menuView.dataSource = self;
  397. menuView.style = self.menuViewStyle;
  398. menuView.layoutMode = self.menuViewLayoutMode;
  399. menuView.progressHeight = self.progressHeight;
  400. menuView.contentMargin = self.menuViewContentMargin;
  401. menuView.progressViewBottomSpace = self.progressViewBottomSpace;
  402. menuView.progressWidths = self.progressViewWidths;
  403. menuView.progressViewIsNaughty = self.progressViewIsNaughty;
  404. menuView.progressViewCornerRadius = self.progressViewCornerRadius;
  405. menuView.showOnNavigationBar = self.showOnNavigationBar;
  406. if (self.titleFontName) {
  407. menuView.fontName = self.titleFontName;
  408. }
  409. if (self.progressColor) {
  410. menuView.lineColor = self.progressColor;
  411. }
  412. if (self.showOnNavigationBar && self.navigationController.navigationBar) {
  413. self.navigationItem.titleView = menuView;
  414. } else {
  415. [self.view addSubview:menuView];
  416. }
  417. self.menuView = menuView;
  418. }
  419. - (void)wm_layoutChildViewControllers {
  420. int currentPage = (int)(self.scrollView.contentOffset.x / _contentViewFrame.size.width);
  421. int length = (int)self.preloadPolicy;
  422. int left = currentPage - length - 1;
  423. int right = currentPage + length + 1;
  424. for (int i = 0; i < self.childControllersCount; i++) {
  425. UIViewController *vc = [self.displayVC objectForKey:@(i)];
  426. CGRect frame = [self.childViewFrames[i] CGRectValue];
  427. if (!vc) {
  428. if ([self wm_isInScreen:frame]) {
  429. [self wm_initializedControllerWithIndexIfNeeded:i];
  430. }
  431. } else if (i <= left || i >= right) {
  432. if (![self wm_isInScreen:frame]) {
  433. [self wm_removeViewController:vc atIndex:i];
  434. }
  435. }
  436. }
  437. }
  438. // 创建或从缓存中获取控制器并添加到视图上
  439. - (void)wm_initializedControllerWithIndexIfNeeded:(NSInteger)index {
  440. // 先从 cache 中取
  441. UIViewController *vc = [self.memCache objectForKey:@(index)];
  442. if (vc) {
  443. // cache 中存在,添加到 scrollView 上,并放入display
  444. [self wm_addCachedViewController:vc atIndex:index];
  445. } else {
  446. // cache 中也不存在,创建并添加到display
  447. [self wm_addViewControllerAtIndex:(int)index];
  448. }
  449. [self wm_postAddToSuperViewNotificationWithIndex:(int)index];
  450. }
  451. - (void)wm_addCachedViewController:(UIViewController *)viewController atIndex:(NSInteger)index {
  452. [self addChildViewController:viewController];
  453. viewController.view.frame = [self.childViewFrames[index] CGRectValue];
  454. [viewController didMoveToParentViewController:self];
  455. [self.scrollView addSubview:viewController.view];
  456. [self willEnterController:viewController atIndex:index];
  457. [self.displayVC setObject:viewController forKey:@(index)];
  458. }
  459. // 创建并添加子控制器
  460. - (void)wm_addViewControllerAtIndex:(int)index {
  461. _initializedIndex = index;
  462. UIViewController *viewController = [self initializeViewControllerAtIndex:index];
  463. if (self.values.count == self.childControllersCount && self.keys.count == self.childControllersCount) {
  464. [viewController setValue:self.values[index] forKey:self.keys[index]];
  465. }
  466. [self addChildViewController:viewController];
  467. CGRect frame = self.childViewFrames.count ? [self.childViewFrames[index] CGRectValue] : self.view.frame;
  468. viewController.view.frame = frame;
  469. [viewController didMoveToParentViewController:self];
  470. [self.scrollView addSubview:viewController.view];
  471. [self willEnterController:viewController atIndex:index];
  472. [self.displayVC setObject:viewController forKey:@(index)];
  473. [self wm_backToPositionIfNeeded:viewController atIndex:index];
  474. }
  475. // 移除控制器,且从display中移除
  476. - (void)wm_removeViewController:(UIViewController *)viewController atIndex:(NSInteger)index {
  477. [self wm_rememberPositionIfNeeded:viewController atIndex:index];
  478. [viewController.view removeFromSuperview];
  479. [viewController willMoveToParentViewController:nil];
  480. [viewController removeFromParentViewController];
  481. [self.displayVC removeObjectForKey:@(index)];
  482. // 放入缓存
  483. if (self.cachePolicy == WMPageControllerCachePolicyDisabled) {
  484. return;
  485. }
  486. if (![self.memCache objectForKey:@(index)]) {
  487. [self willCachedController:viewController atIndex:index];
  488. [self.memCache setObject:viewController forKey:@(index)];
  489. }
  490. }
  491. - (void)wm_backToPositionIfNeeded:(UIViewController *)controller atIndex:(NSInteger)index {
  492. #pragma clang diagnostic push
  493. #pragma clang diagnostic ignored"-Wdeprecated-declarations"
  494. if (!self.rememberLocation) return;
  495. #pragma clang diagnostic pop
  496. if ([self.memCache objectForKey:@(index)]) return;
  497. UIScrollView *scrollView = [self wm_isKindOfScrollViewController:controller];
  498. if (scrollView) {
  499. NSValue *pointValue = self.posRecords[@(index)];
  500. if (pointValue) {
  501. CGPoint pos = [pointValue CGPointValue];
  502. [scrollView setContentOffset:pos];
  503. }
  504. }
  505. }
  506. - (void)wm_rememberPositionIfNeeded:(UIViewController *)controller atIndex:(NSInteger)index {
  507. #pragma clang diagnostic push
  508. #pragma clang diagnostic ignored"-Wdeprecated-declarations"
  509. if (!self.rememberLocation) return;
  510. #pragma clang diagnostic pop
  511. UIScrollView *scrollView = [self wm_isKindOfScrollViewController:controller];
  512. if (scrollView) {
  513. CGPoint pos = scrollView.contentOffset;
  514. self.posRecords[@(index)] = [NSValue valueWithCGPoint:pos];
  515. }
  516. }
  517. - (UIScrollView *)wm_isKindOfScrollViewController:(UIViewController *)controller {
  518. UIScrollView *scrollView = nil;
  519. if ([controller.view isKindOfClass:[UIScrollView class]]) {
  520. // Controller的view是scrollView的子类(UITableViewController/UIViewController替换view为scrollView)
  521. scrollView = (UIScrollView *)controller.view;
  522. } else if (controller.view.subviews.count >= 1) {
  523. // Controller的view的subViews[0]存在且是scrollView的子类,并且frame等与view得frame(UICollectionViewController/UIViewController添加UIScrollView)
  524. UIView *view = controller.view.subviews[0];
  525. if ([view isKindOfClass:[UIScrollView class]]) {
  526. scrollView = (UIScrollView *)view;
  527. }
  528. }
  529. return scrollView;
  530. }
  531. - (BOOL)wm_isInScreen:(CGRect)frame {
  532. CGFloat x = frame.origin.x;
  533. CGFloat ScreenWidth = self.scrollView.frame.size.width;
  534. CGFloat contentOffsetX = self.scrollView.contentOffset.x;
  535. if (CGRectGetMaxX(frame) > contentOffsetX && x - contentOffsetX < ScreenWidth) {
  536. return YES;
  537. } else {
  538. return NO;
  539. }
  540. }
  541. - (void)wm_resetMenuView {
  542. if (!self.menuView) {
  543. [self wm_addMenuView];
  544. } else {
  545. [self.menuView reload];
  546. if (self.menuView.userInteractionEnabled == NO) {
  547. self.menuView.userInteractionEnabled = YES;
  548. }
  549. if (self.selectIndex != 0) {
  550. [self.menuView selectItemAtIndex:self.selectIndex];
  551. }
  552. [self.view bringSubviewToFront:self.menuView];
  553. }
  554. }
  555. - (void)wm_growCachePolicyAfterMemoryWarning {
  556. self.cachePolicy = WMPageControllerCachePolicyBalanced;
  557. [self performSelector:@selector(wm_growCachePolicyToHigh) withObject:nil afterDelay:2.0 inModes:@[NSRunLoopCommonModes]];
  558. }
  559. - (void)wm_growCachePolicyToHigh {
  560. self.cachePolicy = WMPageControllerCachePolicyHigh;
  561. }
  562. #pragma mark - Adjust Frame
  563. - (void)wm_adjustScrollViewFrame {
  564. // While rotate at last page, set scroll frame will call `-scrollViewDidScroll:` delegate
  565. // It's not my expectation, so I use `_shouldNotScroll` to lock it.
  566. // Wait for a better solution.
  567. _shouldNotScroll = YES;
  568. CGFloat oldContentOffsetX = self.scrollView.contentOffset.x;
  569. CGFloat contentWidth = self.scrollView.contentSize.width;
  570. self.scrollView.frame = _contentViewFrame;
  571. self.scrollView.contentSize = CGSizeMake(self.childControllersCount * _contentViewFrame.size.width, 0);
  572. CGFloat xContentOffset = contentWidth == 0 ? self.selectIndex * _contentViewFrame.size.width : oldContentOffsetX / contentWidth * self.childControllersCount * _contentViewFrame.size.width;
  573. [self.scrollView setContentOffset:CGPointMake(xContentOffset, 0)];
  574. _shouldNotScroll = NO;
  575. }
  576. - (void)wm_adjustDisplayingViewControllersFrame {
  577. [self.displayVC enumerateKeysAndObjectsUsingBlock:^(NSNumber * _Nonnull key, UIViewController * _Nonnull vc, BOOL * _Nonnull stop) {
  578. NSInteger index = key.integerValue;
  579. CGRect frame = [self.childViewFrames[index] CGRectValue];
  580. vc.view.frame = frame;
  581. }];
  582. }
  583. - (void)wm_adjustMenuViewFrame {
  584. CGFloat oriWidth = self.menuView.frame.size.width;
  585. self.menuView.frame = _menuViewFrame;
  586. [self.menuView resetFrames];
  587. if (oriWidth != self.menuView.frame.size.width) {
  588. [self.menuView refreshContenOffset];
  589. }
  590. }
  591. - (CGFloat)wm_calculateItemWithAtIndex:(NSInteger)index {
  592. NSString *title = [self titleAtIndex:index];
  593. UIFont *titleFont = self.titleFontName ? [UIFont fontWithName:self.titleFontName size:self.titleSizeSelected] : [UIFont systemFontOfSize:self.titleSizeSelected];
  594. NSDictionary *attrs = @{NSFontAttributeName: titleFont};
  595. CGFloat itemWidth = [title boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX) options:(NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading) attributes:attrs context:nil].size.width;
  596. return ceil(itemWidth);
  597. }
  598. - (void)wm_delaySelectIndexIfNeeded {
  599. if (_markedSelectIndex != kWMUndefinedIndex) {
  600. self.selectIndex = (int)_markedSelectIndex;
  601. }
  602. }
  603. #pragma mark - Life Cycle
  604. - (void)viewDidLoad {
  605. [super viewDidLoad];
  606. self.view.backgroundColor = [UIColor whiteColor];
  607. if (!self.childControllersCount) return;
  608. [self wm_calculateSize];
  609. [self wm_addScrollView];
  610. [self wm_initializedControllerWithIndexIfNeeded:self.selectIndex];
  611. self.currentViewController = self.displayVC[@(self.selectIndex)];
  612. [self wm_addMenuView];
  613. [self didEnterController:self.currentViewController atIndex:self.selectIndex];
  614. }
  615. - (void)viewDidLayoutSubviews {
  616. [super viewDidLayoutSubviews];
  617. if (!self.childControllersCount) return;
  618. [self forceLayoutSubviews];
  619. _hasInited = YES;
  620. [self wm_delaySelectIndexIfNeeded];
  621. }
  622. - (void)didReceiveMemoryWarning {
  623. [super didReceiveMemoryWarning];
  624. // Dispose of any resources that can be recreated.
  625. self.memoryWarningCount++;
  626. self.cachePolicy = WMPageControllerCachePolicyLowMemory;
  627. // 取消正在增长的 cache 操作
  628. [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(wm_growCachePolicyAfterMemoryWarning) object:nil];
  629. [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(wm_growCachePolicyToHigh) object:nil];
  630. [self.memCache removeAllObjects];
  631. [self.posRecords removeAllObjects];
  632. self.posRecords = nil;
  633. // 如果收到内存警告次数小于 3,一段时间后切换到模式 Balanced
  634. if (self.memoryWarningCount < 3) {
  635. [self performSelector:@selector(wm_growCachePolicyAfterMemoryWarning) withObject:nil afterDelay:3.0 inModes:@[NSRunLoopCommonModes]];
  636. }
  637. }
  638. #pragma mark - UIScrollView Delegate
  639. - (void)scrollViewDidScroll:(UIScrollView *)scrollView {
  640. if (![scrollView isKindOfClass:WMScrollView.class]) return;
  641. if (_shouldNotScroll || !_hasInited) return;
  642. [self wm_layoutChildViewControllers];
  643. if (_startDragging) {
  644. CGFloat contentOffsetX = scrollView.contentOffset.x;
  645. if (contentOffsetX < 0) {
  646. contentOffsetX = 0;
  647. }
  648. if (contentOffsetX > scrollView.contentSize.width - _contentViewFrame.size.width) {
  649. contentOffsetX = scrollView.contentSize.width - _contentViewFrame.size.width;
  650. }
  651. CGFloat rate = contentOffsetX / _contentViewFrame.size.width;
  652. [self.menuView slideMenuAtProgress:rate];
  653. }
  654. // Fix scrollView.contentOffset.y -> (-20) unexpectedly.
  655. if (scrollView.contentOffset.y == 0) return;
  656. CGPoint contentOffset = scrollView.contentOffset;
  657. contentOffset.y = 0.0;
  658. scrollView.contentOffset = contentOffset;
  659. }
  660. - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
  661. if (![scrollView isKindOfClass:WMScrollView.class]) return;
  662. _startDragging = YES;
  663. self.menuView.userInteractionEnabled = NO;
  664. }
  665. - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
  666. if (![scrollView isKindOfClass:WMScrollView.class]) return;
  667. self.menuView.userInteractionEnabled = YES;
  668. _selectIndex = (int)(scrollView.contentOffset.x / _contentViewFrame.size.width);
  669. self.currentViewController = self.displayVC[@(self.selectIndex)];
  670. [self didEnterController:self.currentViewController atIndex:self.selectIndex];
  671. [self.menuView deselectedItemsIfNeeded];
  672. }
  673. - (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView {
  674. if (![scrollView isKindOfClass:WMScrollView.class]) return;
  675. self.currentViewController = self.displayVC[@(self.selectIndex)];
  676. [self didEnterController:self.currentViewController atIndex:self.selectIndex];
  677. [self.menuView deselectedItemsIfNeeded];
  678. }
  679. - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
  680. if (![scrollView isKindOfClass:WMScrollView.class]) return;
  681. if (!decelerate) {
  682. self.menuView.userInteractionEnabled = YES;
  683. CGFloat rate = _targetX / _contentViewFrame.size.width;
  684. [self.menuView slideMenuAtProgress:rate];
  685. [self.menuView deselectedItemsIfNeeded];
  686. }
  687. }
  688. - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
  689. if (![scrollView isKindOfClass:WMScrollView.class]) return;
  690. _targetX = targetContentOffset->x;
  691. }
  692. #pragma mark - WMMenuView Delegate
  693. - (void)menuView:(WMMenuView *)menu didSelectedIndex:(NSInteger)index currentIndex:(NSInteger)currentIndex {
  694. if (!_hasInited) return;
  695. _selectIndex = (int)index;
  696. _startDragging = NO;
  697. CGPoint targetP = CGPointMake(_contentViewFrame.size.width * index, 0);
  698. [self.scrollView setContentOffset:targetP animated:self.pageAnimatable];
  699. if (self.pageAnimatable) return;
  700. // 由于不触发 -scrollViewDidScroll: 手动处理控制器
  701. UIViewController *currentViewController = self.displayVC[@(currentIndex)];
  702. if (currentViewController) {
  703. [self wm_removeViewController:currentViewController atIndex:currentIndex];
  704. }
  705. [self wm_layoutChildViewControllers];
  706. self.currentViewController = self.displayVC[@(self.selectIndex)];
  707. [self didEnterController:self.currentViewController atIndex:index];
  708. }
  709. - (CGFloat)menuView:(WMMenuView *)menu widthForItemAtIndex:(NSInteger)index {
  710. if (self.automaticallyCalculatesItemWidths) {
  711. return [self wm_calculateItemWithAtIndex:index];
  712. }
  713. if (self.itemsWidths.count == self.childControllersCount) {
  714. return [self.itemsWidths[index] floatValue];
  715. }
  716. return self.menuItemWidth;
  717. }
  718. - (CGFloat)menuView:(WMMenuView *)menu itemMarginAtIndex:(NSInteger)index {
  719. if (self.itemsMargins.count == self.childControllersCount + 1) {
  720. return [self.itemsMargins[index] floatValue];
  721. }
  722. return self.itemMargin;
  723. }
  724. - (CGFloat)menuView:(WMMenuView *)menu titleSizeForState:(WMMenuItemState)state atIndex:(NSInteger)index {
  725. switch (state) {
  726. case WMMenuItemStateSelected: return self.titleSizeSelected;
  727. case WMMenuItemStateNormal: return self.titleSizeNormal;
  728. }
  729. }
  730. - (UIColor *)menuView:(WMMenuView *)menu titleColorForState:(WMMenuItemState)state atIndex:(NSInteger)index {
  731. switch (state) {
  732. case WMMenuItemStateSelected: return self.titleColorSelected;
  733. case WMMenuItemStateNormal: return self.titleColorNormal;
  734. }
  735. }
  736. #pragma mark - WMMenuViewDataSource
  737. - (NSInteger)numbersOfTitlesInMenuView:(WMMenuView *)menu {
  738. return self.childControllersCount;
  739. }
  740. - (NSString *)menuView:(WMMenuView *)menu titleAtIndex:(NSInteger)index {
  741. return [self titleAtIndex:index];
  742. }
  743. #pragma mark - WMPageControllerDataSource
  744. - (CGRect)pageController:(WMPageController *)pageController preferredFrameForMenuView:(WMMenuView *)menuView {
  745. NSAssert(0, @"[%@] MUST IMPLEMENT DATASOURCE METHOD `-pageController:preferredFrameForMenuView:`", [self.dataSource class]);
  746. return CGRectZero;
  747. }
  748. - (CGRect)pageController:(WMPageController *)pageController preferredFrameForContentView:(WMScrollView *)contentView {
  749. NSAssert(0, @"[%@] MUST IMPLEMENT DATASOURCE METHOD `-pageController:preferredFrameForContentView:`", [self.dataSource class]);
  750. return CGRectZero;
  751. }
  752. @end