WMPageController.m 33 KB

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