862 lines
33 KiB
Objective-C
Executable File
862 lines
33 KiB
Objective-C
Executable File
//
|
||
// WMPageController.m
|
||
// WMPageController
|
||
//
|
||
// Created by Mark on 15/6/11.
|
||
// Copyright (c) 2015年 yq. All rights reserved.
|
||
//
|
||
|
||
#import "WMPageController.h"
|
||
|
||
NSString *const WMControllerDidAddToSuperViewNotification = @"WMControllerDidAddToSuperViewNotification";
|
||
NSString *const WMControllerDidFullyDisplayedNotification = @"WMControllerDidFullyDisplayedNotification";
|
||
|
||
static NSInteger const kWMUndefinedIndex = -1;
|
||
static NSInteger const kWMControllerCountUndefined = -1;
|
||
@interface WMPageController () {
|
||
CGFloat _targetX;
|
||
CGRect _contentViewFrame, _menuViewFrame;
|
||
BOOL _hasInited, _shouldNotScroll;
|
||
NSInteger _initializedIndex, _controllerCount, _markedSelectIndex;
|
||
}
|
||
@property (nonatomic, strong, readwrite) UIViewController *currentViewController;
|
||
// 用于记录子控制器view的frame,用于 scrollView 上的展示的位置
|
||
@property (nonatomic, strong) NSMutableArray *childViewFrames;
|
||
// 当前展示在屏幕上的控制器,方便在滚动的时候读取 (避免不必要计算)
|
||
@property (nonatomic, strong) NSMutableDictionary *displayVC;
|
||
// 用于记录销毁的viewController的位置 (如果它是某一种scrollView的Controller的话)
|
||
@property (nonatomic, strong) NSMutableDictionary *posRecords;
|
||
// 用于缓存加载过的控制器
|
||
@property (nonatomic, strong) NSCache *memCache;
|
||
@property (nonatomic, strong) NSMutableDictionary *backgroundCache;
|
||
// 收到内存警告的次数
|
||
@property (nonatomic, assign) int memoryWarningCount;
|
||
@property (nonatomic, readonly) NSInteger childControllersCount;
|
||
@end
|
||
|
||
@implementation WMPageController
|
||
|
||
#pragma mark - Lazy Loading
|
||
- (NSMutableDictionary *)posRecords {
|
||
if (_posRecords == nil) {
|
||
_posRecords = [[NSMutableDictionary alloc] init];
|
||
}
|
||
return _posRecords;
|
||
}
|
||
|
||
- (NSMutableDictionary *)displayVC {
|
||
if (_displayVC == nil) {
|
||
_displayVC = [[NSMutableDictionary alloc] init];
|
||
}
|
||
return _displayVC;
|
||
}
|
||
|
||
- (NSMutableDictionary *)backgroundCache {
|
||
if (_backgroundCache == nil) {
|
||
_backgroundCache = [[NSMutableDictionary alloc] init];
|
||
}
|
||
return _backgroundCache;
|
||
}
|
||
|
||
#pragma mark - Public Methods
|
||
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
|
||
if (self = [super initWithCoder:aDecoder]) {
|
||
[self wm_setup];
|
||
}
|
||
return self;
|
||
}
|
||
|
||
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
|
||
if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) {
|
||
[self wm_setup];
|
||
}
|
||
return self;
|
||
}
|
||
|
||
- (instancetype)initWithViewControllerClasses:(NSArray<Class> *)classes andTheirTitles:(NSArray<NSString *> *)titles {
|
||
if (self = [self initWithNibName:nil bundle:nil]) {
|
||
NSParameterAssert(classes.count == titles.count);
|
||
_viewControllerClasses = [NSArray arrayWithArray:classes];
|
||
_titles = [NSArray arrayWithArray:titles];
|
||
}
|
||
return self;
|
||
}
|
||
|
||
- (void)dealloc {
|
||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(wm_growCachePolicyAfterMemoryWarning) object:nil];
|
||
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(wm_growCachePolicyToHigh) object:nil];
|
||
}
|
||
|
||
- (void)forceLayoutSubviews {
|
||
if (!self.childControllersCount) return;
|
||
// 计算宽高及子控制器的视图frame
|
||
[self wm_calculateSize];
|
||
[self wm_adjustScrollViewFrame];
|
||
[self wm_adjustMenuViewFrame];
|
||
[self wm_adjustDisplayingViewControllersFrame];
|
||
}
|
||
|
||
- (void)setScrollEnable:(BOOL)scrollEnable {
|
||
_scrollEnable = scrollEnable;
|
||
|
||
if (!self.scrollView) return;
|
||
self.scrollView.scrollEnabled = scrollEnable;
|
||
}
|
||
|
||
- (void)setProgressViewCornerRadius:(CGFloat)progressViewCornerRadius {
|
||
_progressViewCornerRadius = progressViewCornerRadius;
|
||
if (self.menuView) {
|
||
self.menuView.progressViewCornerRadius = progressViewCornerRadius;
|
||
}
|
||
}
|
||
|
||
- (void)setMenuViewLayoutMode:(WMMenuViewLayoutMode)menuViewLayoutMode {
|
||
_menuViewLayoutMode = menuViewLayoutMode;
|
||
if (self.menuView.superview) {
|
||
[self wm_resetMenuView];
|
||
}
|
||
}
|
||
|
||
- (void)setCachePolicy:(WMPageControllerCachePolicy)cachePolicy {
|
||
_cachePolicy = cachePolicy;
|
||
if (cachePolicy != WMPageControllerCachePolicyDisabled) {
|
||
self.memCache.countLimit = _cachePolicy;
|
||
}
|
||
}
|
||
|
||
- (void)setSelectIndex:(int)selectIndex {
|
||
_selectIndex = selectIndex;
|
||
_markedSelectIndex = kWMUndefinedIndex;
|
||
if (self.menuView && _hasInited) {
|
||
[self.menuView selectItemAtIndex:selectIndex];
|
||
} else {
|
||
_markedSelectIndex = selectIndex;
|
||
UIViewController *vc = [self.memCache objectForKey:@(selectIndex)];
|
||
if (!vc) {
|
||
vc = [self initializeViewControllerAtIndex:selectIndex];
|
||
[self.memCache setObject:vc forKey:@(selectIndex)];
|
||
}
|
||
self.currentViewController = vc;
|
||
}
|
||
}
|
||
|
||
- (void)setProgressViewIsNaughty:(BOOL)progressViewIsNaughty {
|
||
_progressViewIsNaughty = progressViewIsNaughty;
|
||
if (self.menuView) {
|
||
self.menuView.progressViewIsNaughty = progressViewIsNaughty;
|
||
}
|
||
}
|
||
|
||
- (void)setProgressWidth:(CGFloat)progressWidth {
|
||
_progressWidth = progressWidth;
|
||
self.progressViewWidths = ({
|
||
NSMutableArray *tmp = [NSMutableArray array];
|
||
for (int i = 0; i < self.childControllersCount; i++) {
|
||
[tmp addObject:@(progressWidth)];
|
||
}
|
||
tmp.copy;
|
||
});
|
||
}
|
||
|
||
- (void)setProgressViewWidths:(NSArray *)progressViewWidths {
|
||
_progressViewWidths = progressViewWidths;
|
||
if (self.menuView) {
|
||
self.menuView.progressWidths = progressViewWidths;
|
||
}
|
||
}
|
||
|
||
- (void)setMenuViewContentMargin:(CGFloat)menuViewContentMargin {
|
||
_menuViewContentMargin = menuViewContentMargin;
|
||
if (self.menuView) {
|
||
self.menuView.contentMargin = menuViewContentMargin;
|
||
}
|
||
}
|
||
|
||
- (void)reloadData {
|
||
[self wm_clearDatas];
|
||
|
||
if (!self.childControllersCount) return;
|
||
|
||
[self wm_resetScrollView];
|
||
[self.memCache removeAllObjects];
|
||
[self wm_resetMenuView];
|
||
[self viewDidLayoutSubviews];
|
||
[self didEnterController:self.currentViewController atIndex:self.selectIndex];
|
||
}
|
||
|
||
- (void)updateTitle:(NSString *)title atIndex:(NSInteger)index {
|
||
[self.menuView updateTitle:title atIndex:index andWidth:NO];
|
||
}
|
||
|
||
- (void)updateAttributeTitle:(NSAttributedString * _Nonnull)title atIndex:(NSInteger)index {
|
||
[self.menuView updateAttributeTitle:title atIndex:index andWidth:NO];
|
||
}
|
||
|
||
- (void)updateTitle:(NSString *)title andWidth:(CGFloat)width atIndex:(NSInteger)index {
|
||
if (self.itemsWidths && index < self.itemsWidths.count) {
|
||
NSMutableArray *mutableWidths = [NSMutableArray arrayWithArray:self.itemsWidths];
|
||
mutableWidths[index] = @(width);
|
||
self.itemsWidths = [mutableWidths copy];
|
||
} else {
|
||
NSMutableArray *mutableWidths = [NSMutableArray array];
|
||
for (int i = 0; i < self.childControllersCount; i++) {
|
||
CGFloat itemWidth = (i == index) ? width : self.menuItemWidth;
|
||
[mutableWidths addObject:@(itemWidth)];
|
||
}
|
||
self.itemsWidths = [mutableWidths copy];
|
||
}
|
||
[self.menuView updateTitle:title atIndex:index andWidth:YES];
|
||
}
|
||
|
||
- (void)setShowOnNavigationBar:(BOOL)showOnNavigationBar {
|
||
if (_showOnNavigationBar == showOnNavigationBar) {
|
||
return;
|
||
}
|
||
|
||
_showOnNavigationBar = showOnNavigationBar;
|
||
if (self.menuView) {
|
||
[self.menuView removeFromSuperview];
|
||
[self wm_addMenuView];
|
||
[self forceLayoutSubviews];
|
||
[self.menuView slideMenuAtProgress:self.selectIndex];
|
||
}
|
||
}
|
||
|
||
#pragma mark - Notification
|
||
- (void)willResignActive:(NSNotification *)notification {
|
||
for (int i = 0; i < self.childControllersCount; i++) {
|
||
id obj = [self.memCache objectForKey:@(i)];
|
||
if (obj) {
|
||
[self.backgroundCache setObject:obj forKey:@(i)];
|
||
}
|
||
}
|
||
}
|
||
|
||
- (void)willEnterForeground:(NSNotification *)notification {
|
||
for (NSNumber *key in self.backgroundCache.allKeys) {
|
||
if (![self.memCache objectForKey:key]) {
|
||
[self.memCache setObject:self.backgroundCache[key] forKey:key];
|
||
}
|
||
}
|
||
[self.backgroundCache removeAllObjects];
|
||
}
|
||
|
||
#pragma mark - Delegate
|
||
- (NSDictionary *)infoWithIndex:(NSInteger)index {
|
||
NSString *title = [self titleAtIndex:index];
|
||
return @{@"title": title ?: @"", @"index": @(index)};
|
||
}
|
||
|
||
- (void)willCachedController:(UIViewController *)vc atIndex:(NSInteger)index {
|
||
if (self.childControllersCount && [self.delegate respondsToSelector:@selector(pageController:willCachedViewController:withInfo:)]) {
|
||
NSDictionary *info = [self infoWithIndex:index];
|
||
[self.delegate pageController:self willCachedViewController:vc withInfo:info];
|
||
}
|
||
}
|
||
|
||
- (void)willEnterController:(UIViewController *)vc atIndex:(NSInteger)index {
|
||
_selectIndex = (int)index;
|
||
if (self.childControllersCount && [self.delegate respondsToSelector:@selector(pageController:willEnterViewController:withInfo:)]) {
|
||
NSDictionary *info = [self infoWithIndex:index];
|
||
[self.delegate pageController:self willEnterViewController:vc withInfo:info];
|
||
}
|
||
}
|
||
|
||
// 完全进入控制器 (即停止滑动后调用)
|
||
- (void)didEnterController:(UIViewController *)vc atIndex:(NSInteger)index {
|
||
if (!self.childControllersCount) return;
|
||
|
||
// Post FullyDisplayedNotification
|
||
[self wm_postFullyDisplayedNotificationWithCurrentIndex:self.selectIndex];
|
||
|
||
NSDictionary *info = [self infoWithIndex:index];
|
||
if ([self.delegate respondsToSelector:@selector(pageController:didEnterViewController:withInfo:)]) {
|
||
[self.delegate pageController:self didEnterViewController:vc withInfo:info];
|
||
}
|
||
|
||
// 当控制器创建时,调用延迟加载的代理方法
|
||
if (_initializedIndex == index && [self.delegate respondsToSelector:@selector(pageController:lazyLoadViewController:withInfo:)]) {
|
||
[self.delegate pageController:self lazyLoadViewController:vc withInfo:info];
|
||
_initializedIndex = kWMUndefinedIndex;
|
||
}
|
||
|
||
// 根据 preloadPolicy 预加载控制器
|
||
if (self.preloadPolicy == WMPageControllerPreloadPolicyNever) return;
|
||
int length = (int)self.preloadPolicy;
|
||
int start = 0;
|
||
int end = (int)self.childControllersCount - 1;
|
||
if (index > length) {
|
||
start = (int)index - length;
|
||
}
|
||
if (self.childControllersCount - 1 > length + index) {
|
||
end = (int)index + length;
|
||
}
|
||
for (int i = start; i <= end; i++) {
|
||
// 如果已存在,不需要预加载
|
||
if (![self.memCache objectForKey:@(i)] && !self.displayVC[@(i)]) {
|
||
[self wm_addViewControllerAtIndex:i];
|
||
[self wm_postAddToSuperViewNotificationWithIndex:i];
|
||
}
|
||
}
|
||
_selectIndex = (int)index;
|
||
}
|
||
|
||
#pragma mark - Data source
|
||
- (NSInteger)childControllersCount {
|
||
if (_controllerCount == kWMControllerCountUndefined) {
|
||
if ([self.dataSource respondsToSelector:@selector(numbersOfChildControllersInPageController:)]) {
|
||
_controllerCount = [self.dataSource numbersOfChildControllersInPageController:self];
|
||
} else {
|
||
_controllerCount = self.viewControllerClasses.count;
|
||
}
|
||
}
|
||
return _controllerCount;
|
||
}
|
||
|
||
- (UIViewController * _Nonnull)initializeViewControllerAtIndex:(NSInteger)index {
|
||
if ([self.dataSource respondsToSelector:@selector(pageController:viewControllerAtIndex:)]) {
|
||
return [self.dataSource pageController:self viewControllerAtIndex:index];
|
||
}
|
||
return [[self.viewControllerClasses[index] alloc] init];
|
||
}
|
||
|
||
- (NSString * _Nonnull)titleAtIndex:(NSInteger)index {
|
||
NSString *title = nil;
|
||
if ([self.dataSource respondsToSelector:@selector(pageController:titleAtIndex:)]) {
|
||
title = [self.dataSource pageController:self titleAtIndex:index];
|
||
} else {
|
||
title = self.titles[index];
|
||
}
|
||
return (title ?: @"");
|
||
}
|
||
|
||
#pragma mark - Private Methods
|
||
|
||
- (void)wm_resetScrollView {
|
||
if (self.scrollView) {
|
||
[self.scrollView removeFromSuperview];
|
||
}
|
||
[self wm_addScrollView];
|
||
[self wm_addViewControllerAtIndex:self.selectIndex];
|
||
self.currentViewController = self.displayVC[@(self.selectIndex)];
|
||
}
|
||
|
||
- (void)wm_clearDatas {
|
||
_controllerCount = kWMControllerCountUndefined;
|
||
_hasInited = NO;
|
||
NSUInteger maxIndex = (self.childControllersCount - 1 > 0) ? (self.childControllersCount - 1) : 0;
|
||
_selectIndex = self.selectIndex < self.childControllersCount ? self.selectIndex : (int)maxIndex;
|
||
if (self.progressWidth > 0) { self.progressWidth = self.progressWidth; }
|
||
|
||
NSArray *displayingViewControllers = self.displayVC.allValues;
|
||
for (UIViewController *vc in displayingViewControllers) {
|
||
[vc.view removeFromSuperview];
|
||
[vc willMoveToParentViewController:nil];
|
||
[vc removeFromParentViewController];
|
||
}
|
||
self.memoryWarningCount = 0;
|
||
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(wm_growCachePolicyAfterMemoryWarning) object:nil];
|
||
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(wm_growCachePolicyToHigh) object:nil];
|
||
self.currentViewController = nil;
|
||
[self.posRecords removeAllObjects];
|
||
[self.displayVC removeAllObjects];
|
||
}
|
||
|
||
// 当子控制器init完成时发送通知
|
||
- (void)wm_postAddToSuperViewNotificationWithIndex:(int)index {
|
||
if (!self.postNotification) return;
|
||
NSDictionary *info = @{
|
||
@"index":@(index),
|
||
@"title":[self titleAtIndex:index]
|
||
};
|
||
[[NSNotificationCenter defaultCenter] postNotificationName:WMControllerDidAddToSuperViewNotification
|
||
object:self
|
||
userInfo:info];
|
||
}
|
||
|
||
// 当子控制器完全展示在user面前时发送通知
|
||
- (void)wm_postFullyDisplayedNotificationWithCurrentIndex:(int)index {
|
||
if (!self.postNotification) return;
|
||
NSDictionary *info = @{
|
||
@"index":@(index),
|
||
@"title":[self titleAtIndex:index]
|
||
};
|
||
[[NSNotificationCenter defaultCenter] postNotificationName:WMControllerDidFullyDisplayedNotification
|
||
object:self
|
||
userInfo:info];
|
||
}
|
||
|
||
// 初始化一些参数,在init中调用
|
||
- (void)wm_setup {
|
||
_titleSizeSelected = 18.0f;
|
||
_titleSizeNormal = 15.0f;
|
||
_titleColorSelected = [UIColor colorWithRed:168.0/255.0 green:20.0/255.0 blue:4/255.0 alpha:1];
|
||
_titleColorNormal = [UIColor colorWithRed:0 green:0 blue:0 alpha:1];
|
||
_menuItemWidth = 65.0f;
|
||
|
||
_memCache = [[NSCache alloc] init];
|
||
_initializedIndex = kWMUndefinedIndex;
|
||
_markedSelectIndex = kWMUndefinedIndex;
|
||
_controllerCount = kWMControllerCountUndefined;
|
||
_scrollEnable = YES;
|
||
|
||
self.automaticallyCalculatesItemWidths = NO;
|
||
self.automaticallyAdjustsScrollViewInsets = NO;
|
||
self.preloadPolicy = WMPageControllerPreloadPolicyNever;
|
||
self.cachePolicy = WMPageControllerCachePolicyNoLimit;
|
||
|
||
self.delegate = self;
|
||
self.dataSource = self;
|
||
|
||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(willResignActive:) name:UIApplicationWillResignActiveNotification object:nil];
|
||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(willEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil];
|
||
}
|
||
|
||
// 包括宽高,子控制器视图 frame
|
||
- (void)wm_calculateSize {
|
||
_menuViewFrame = [self.dataSource pageController:self preferredFrameForMenuView:self.menuView];
|
||
_contentViewFrame = [self.dataSource pageController:self preferredFrameForContentView:self.scrollView];
|
||
_childViewFrames = [NSMutableArray array];
|
||
for (int i = 0; i < self.childControllersCount; i++) {
|
||
CGRect frame = CGRectMake(i * _contentViewFrame.size.width, 0, _contentViewFrame.size.width, _contentViewFrame.size.height);
|
||
[_childViewFrames addObject:[NSValue valueWithCGRect:frame]];
|
||
}
|
||
}
|
||
|
||
- (void)wm_addScrollView {
|
||
WMScrollView *scrollView = [[WMScrollView alloc] init];
|
||
scrollView.scrollsToTop = NO;
|
||
scrollView.pagingEnabled = YES;
|
||
scrollView.backgroundColor = [UIColor clearColor];
|
||
scrollView.delegate = self;
|
||
scrollView.showsVerticalScrollIndicator = NO;
|
||
scrollView.showsHorizontalScrollIndicator = NO;
|
||
scrollView.bounces = self.bounces;
|
||
scrollView.scrollEnabled = self.scrollEnable;
|
||
if (@available(iOS 11.0, *)) {
|
||
scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
|
||
}
|
||
[self.view addSubview:scrollView];
|
||
self.scrollView = scrollView;
|
||
|
||
if (!self.navigationController) return;
|
||
for (UIGestureRecognizer *gestureRecognizer in scrollView.gestureRecognizers) {
|
||
[gestureRecognizer requireGestureRecognizerToFail:self.navigationController.interactivePopGestureRecognizer];
|
||
}
|
||
}
|
||
|
||
- (void)wm_addMenuView {
|
||
WMMenuView *menuView = [[WMMenuView alloc] initWithFrame:CGRectZero];
|
||
menuView.delegate = self;
|
||
menuView.dataSource = self;
|
||
menuView.style = self.menuViewStyle;
|
||
menuView.layoutMode = self.menuViewLayoutMode;
|
||
menuView.progressHeight = self.progressHeight;
|
||
menuView.contentMargin = self.menuViewContentMargin;
|
||
menuView.progressViewBottomSpace = self.progressViewBottomSpace;
|
||
menuView.progressWidths = self.progressViewWidths;
|
||
menuView.progressViewIsNaughty = self.progressViewIsNaughty;
|
||
menuView.progressViewCornerRadius = self.progressViewCornerRadius;
|
||
menuView.showOnNavigationBar = self.showOnNavigationBar;
|
||
if (self.titleFontName) {
|
||
menuView.fontName = self.titleFontName;
|
||
}
|
||
if (self.progressColor) {
|
||
menuView.lineColor = self.progressColor;
|
||
}
|
||
if (self.showOnNavigationBar && self.navigationController.navigationBar) {
|
||
self.navigationItem.titleView = menuView;
|
||
} else {
|
||
[self.view addSubview:menuView];
|
||
}
|
||
self.menuView = menuView;
|
||
}
|
||
|
||
- (void)wm_layoutChildViewControllers {
|
||
int currentPage = (int)(self.scrollView.contentOffset.x / _contentViewFrame.size.width);
|
||
int length = (int)self.preloadPolicy;
|
||
int left = currentPage - length - 1;
|
||
int right = currentPage + length + 1;
|
||
for (int i = 0; i < self.childControllersCount; i++) {
|
||
UIViewController *vc = [self.displayVC objectForKey:@(i)];
|
||
CGRect frame = [self.childViewFrames[i] CGRectValue];
|
||
if (!vc) {
|
||
if ([self wm_isInScreen:frame]) {
|
||
[self wm_initializedControllerWithIndexIfNeeded:i];
|
||
}
|
||
} else if (i <= left || i >= right) {
|
||
if (![self wm_isInScreen:frame]) {
|
||
[self wm_removeViewController:vc atIndex:i];
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 创建或从缓存中获取控制器并添加到视图上
|
||
- (void)wm_initializedControllerWithIndexIfNeeded:(NSInteger)index {
|
||
// 先从 cache 中取
|
||
UIViewController *vc = [self.memCache objectForKey:@(index)];
|
||
if (vc) {
|
||
// cache 中存在,添加到 scrollView 上,并放入display
|
||
[self wm_addCachedViewController:vc atIndex:index];
|
||
} else {
|
||
// cache 中也不存在,创建并添加到display
|
||
[self wm_addViewControllerAtIndex:(int)index];
|
||
}
|
||
[self wm_postAddToSuperViewNotificationWithIndex:(int)index];
|
||
}
|
||
|
||
- (void)wm_addCachedViewController:(UIViewController *)viewController atIndex:(NSInteger)index {
|
||
[self addChildViewController:viewController];
|
||
viewController.view.frame = [self.childViewFrames[index] CGRectValue];
|
||
[viewController didMoveToParentViewController:self];
|
||
[self.scrollView addSubview:viewController.view];
|
||
[self willEnterController:viewController atIndex:index];
|
||
[self.displayVC setObject:viewController forKey:@(index)];
|
||
}
|
||
|
||
// 创建并添加子控制器
|
||
- (void)wm_addViewControllerAtIndex:(int)index {
|
||
_initializedIndex = index;
|
||
UIViewController *viewController = [self initializeViewControllerAtIndex:index];
|
||
if (self.values.count == self.childControllersCount && self.keys.count == self.childControllersCount) {
|
||
[viewController setValue:self.values[index] forKey:self.keys[index]];
|
||
}
|
||
[self addChildViewController:viewController];
|
||
CGRect frame = self.childViewFrames.count ? [self.childViewFrames[index] CGRectValue] : self.view.frame;
|
||
viewController.view.frame = frame;
|
||
[viewController didMoveToParentViewController:self];
|
||
[self.scrollView addSubview:viewController.view];
|
||
[self willEnterController:viewController atIndex:index];
|
||
[self.displayVC setObject:viewController forKey:@(index)];
|
||
|
||
[self wm_backToPositionIfNeeded:viewController atIndex:index];
|
||
}
|
||
|
||
// 移除控制器,且从display中移除
|
||
- (void)wm_removeViewController:(UIViewController *)viewController atIndex:(NSInteger)index {
|
||
[self wm_rememberPositionIfNeeded:viewController atIndex:index];
|
||
[viewController.view removeFromSuperview];
|
||
[viewController willMoveToParentViewController:nil];
|
||
[viewController removeFromParentViewController];
|
||
[self.displayVC removeObjectForKey:@(index)];
|
||
|
||
// 放入缓存
|
||
if (self.cachePolicy == WMPageControllerCachePolicyDisabled) {
|
||
return;
|
||
}
|
||
|
||
if (![self.memCache objectForKey:@(index)]) {
|
||
[self willCachedController:viewController atIndex:index];
|
||
[self.memCache setObject:viewController forKey:@(index)];
|
||
}
|
||
}
|
||
|
||
- (void)wm_backToPositionIfNeeded:(UIViewController *)controller atIndex:(NSInteger)index {
|
||
#pragma clang diagnostic push
|
||
#pragma clang diagnostic ignored"-Wdeprecated-declarations"
|
||
if (!self.rememberLocation) return;
|
||
#pragma clang diagnostic pop
|
||
if ([self.memCache objectForKey:@(index)]) return;
|
||
UIScrollView *scrollView = [self wm_isKindOfScrollViewController:controller];
|
||
if (scrollView) {
|
||
NSValue *pointValue = self.posRecords[@(index)];
|
||
if (pointValue) {
|
||
CGPoint pos = [pointValue CGPointValue];
|
||
[scrollView setContentOffset:pos];
|
||
}
|
||
}
|
||
}
|
||
|
||
- (void)wm_rememberPositionIfNeeded:(UIViewController *)controller atIndex:(NSInteger)index {
|
||
#pragma clang diagnostic push
|
||
#pragma clang diagnostic ignored"-Wdeprecated-declarations"
|
||
if (!self.rememberLocation) return;
|
||
#pragma clang diagnostic pop
|
||
UIScrollView *scrollView = [self wm_isKindOfScrollViewController:controller];
|
||
if (scrollView) {
|
||
CGPoint pos = scrollView.contentOffset;
|
||
self.posRecords[@(index)] = [NSValue valueWithCGPoint:pos];
|
||
}
|
||
}
|
||
|
||
- (UIScrollView *)wm_isKindOfScrollViewController:(UIViewController *)controller {
|
||
UIScrollView *scrollView = nil;
|
||
if ([controller.view isKindOfClass:[UIScrollView class]]) {
|
||
// Controller的view是scrollView的子类(UITableViewController/UIViewController替换view为scrollView)
|
||
scrollView = (UIScrollView *)controller.view;
|
||
} else if (controller.view.subviews.count >= 1) {
|
||
// Controller的view的subViews[0]存在且是scrollView的子类,并且frame等与view得frame(UICollectionViewController/UIViewController添加UIScrollView)
|
||
UIView *view = controller.view.subviews[0];
|
||
if ([view isKindOfClass:[UIScrollView class]]) {
|
||
scrollView = (UIScrollView *)view;
|
||
}
|
||
}
|
||
return scrollView;
|
||
}
|
||
|
||
- (BOOL)wm_isInScreen:(CGRect)frame {
|
||
CGFloat x = frame.origin.x;
|
||
CGFloat ScreenWidth = self.scrollView.frame.size.width;
|
||
|
||
CGFloat contentOffsetX = self.scrollView.contentOffset.x;
|
||
if (CGRectGetMaxX(frame) > contentOffsetX && x - contentOffsetX < ScreenWidth) {
|
||
return YES;
|
||
} else {
|
||
return NO;
|
||
}
|
||
}
|
||
|
||
- (void)wm_resetMenuView {
|
||
if (!self.menuView) {
|
||
[self wm_addMenuView];
|
||
} else {
|
||
[self.menuView reload];
|
||
if (self.menuView.userInteractionEnabled == NO) {
|
||
self.menuView.userInteractionEnabled = YES;
|
||
}
|
||
if (self.selectIndex != 0) {
|
||
[self.menuView selectItemAtIndex:self.selectIndex];
|
||
}
|
||
[self.view bringSubviewToFront:self.menuView];
|
||
}
|
||
}
|
||
|
||
- (void)wm_growCachePolicyAfterMemoryWarning {
|
||
self.cachePolicy = WMPageControllerCachePolicyBalanced;
|
||
[self performSelector:@selector(wm_growCachePolicyToHigh) withObject:nil afterDelay:2.0 inModes:@[NSRunLoopCommonModes]];
|
||
}
|
||
|
||
- (void)wm_growCachePolicyToHigh {
|
||
self.cachePolicy = WMPageControllerCachePolicyHigh;
|
||
}
|
||
|
||
#pragma mark - Adjust Frame
|
||
- (void)wm_adjustScrollViewFrame {
|
||
// While rotate at last page, set scroll frame will call `-scrollViewDidScroll:` delegate
|
||
// It's not my expectation, so I use `_shouldNotScroll` to lock it.
|
||
// Wait for a better solution.
|
||
_shouldNotScroll = YES;
|
||
CGFloat oldContentOffsetX = self.scrollView.contentOffset.x;
|
||
CGFloat contentWidth = self.scrollView.contentSize.width;
|
||
self.scrollView.frame = _contentViewFrame;
|
||
self.scrollView.contentSize = CGSizeMake(self.childControllersCount * _contentViewFrame.size.width, 0);
|
||
CGFloat xContentOffset = contentWidth == 0 ? self.selectIndex * _contentViewFrame.size.width : oldContentOffsetX / contentWidth * self.childControllersCount * _contentViewFrame.size.width;
|
||
[self.scrollView setContentOffset:CGPointMake(xContentOffset, 0)];
|
||
_shouldNotScroll = NO;
|
||
}
|
||
|
||
- (void)wm_adjustDisplayingViewControllersFrame {
|
||
[self.displayVC enumerateKeysAndObjectsUsingBlock:^(NSNumber * _Nonnull key, UIViewController * _Nonnull vc, BOOL * _Nonnull stop) {
|
||
NSInteger index = key.integerValue;
|
||
CGRect frame = [self.childViewFrames[index] CGRectValue];
|
||
vc.view.frame = frame;
|
||
}];
|
||
}
|
||
|
||
- (void)wm_adjustMenuViewFrame {
|
||
CGFloat oriWidth = self.menuView.frame.size.width;
|
||
self.menuView.frame = _menuViewFrame;
|
||
[self.menuView resetFrames];
|
||
if (oriWidth != self.menuView.frame.size.width) {
|
||
[self.menuView refreshContenOffset];
|
||
}
|
||
}
|
||
|
||
- (CGFloat)wm_calculateItemWithAtIndex:(NSInteger)index {
|
||
NSString *title = [self titleAtIndex:index];
|
||
UIFont *titleFont = self.titleFontName ? [UIFont fontWithName:self.titleFontName size:self.titleSizeSelected] : [UIFont systemFontOfSize:self.titleSizeSelected];
|
||
NSDictionary *attrs = @{NSFontAttributeName: titleFont};
|
||
CGFloat itemWidth = [title boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX) options:(NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading) attributes:attrs context:nil].size.width;
|
||
return ceil(itemWidth);
|
||
}
|
||
|
||
- (void)wm_delaySelectIndexIfNeeded {
|
||
if (_markedSelectIndex != kWMUndefinedIndex) {
|
||
self.selectIndex = (int)_markedSelectIndex;
|
||
}
|
||
}
|
||
|
||
#pragma mark - Life Cycle
|
||
- (void)viewDidLoad {
|
||
[super viewDidLoad];
|
||
self.view.backgroundColor = [UIColor clearColor];
|
||
if (!self.childControllersCount) return;
|
||
[self wm_calculateSize];
|
||
[self wm_addScrollView];
|
||
[self wm_initializedControllerWithIndexIfNeeded:self.selectIndex];
|
||
self.currentViewController = self.displayVC[@(self.selectIndex)];
|
||
[self wm_addMenuView];
|
||
[self didEnterController:self.currentViewController atIndex:self.selectIndex];
|
||
}
|
||
|
||
- (void)viewDidLayoutSubviews {
|
||
[super viewDidLayoutSubviews];
|
||
|
||
if (!self.childControllersCount) return;
|
||
[self forceLayoutSubviews];
|
||
_hasInited = YES;
|
||
[self wm_delaySelectIndexIfNeeded];
|
||
}
|
||
|
||
- (void)didReceiveMemoryWarning {
|
||
[super didReceiveMemoryWarning];
|
||
// Dispose of any resources that can be recreated.
|
||
self.memoryWarningCount++;
|
||
self.cachePolicy = WMPageControllerCachePolicyLowMemory;
|
||
// 取消正在增长的 cache 操作
|
||
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(wm_growCachePolicyAfterMemoryWarning) object:nil];
|
||
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(wm_growCachePolicyToHigh) object:nil];
|
||
|
||
[self.memCache removeAllObjects];
|
||
[self.posRecords removeAllObjects];
|
||
self.posRecords = nil;
|
||
|
||
// 如果收到内存警告次数小于 3,一段时间后切换到模式 Balanced
|
||
if (self.memoryWarningCount < 3) {
|
||
[self performSelector:@selector(wm_growCachePolicyAfterMemoryWarning) withObject:nil afterDelay:3.0 inModes:@[NSRunLoopCommonModes]];
|
||
}
|
||
}
|
||
|
||
#pragma mark - UIScrollView Delegate
|
||
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
|
||
if (![scrollView isKindOfClass:WMScrollView.class]) return;
|
||
|
||
if (_shouldNotScroll || !_hasInited) return;
|
||
|
||
[self wm_layoutChildViewControllers];
|
||
if (_startDragging) {
|
||
CGFloat contentOffsetX = scrollView.contentOffset.x;
|
||
if (contentOffsetX < 0) {
|
||
contentOffsetX = 0;
|
||
}
|
||
if (contentOffsetX > scrollView.contentSize.width - _contentViewFrame.size.width) {
|
||
contentOffsetX = scrollView.contentSize.width - _contentViewFrame.size.width;
|
||
}
|
||
CGFloat rate = contentOffsetX / _contentViewFrame.size.width;
|
||
[self.menuView slideMenuAtProgress:rate];
|
||
}
|
||
|
||
// Fix scrollView.contentOffset.y -> (-20) unexpectedly.
|
||
if (scrollView.contentOffset.y == 0) return;
|
||
CGPoint contentOffset = scrollView.contentOffset;
|
||
contentOffset.y = 0.0;
|
||
scrollView.contentOffset = contentOffset;
|
||
}
|
||
|
||
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
|
||
if (![scrollView isKindOfClass:WMScrollView.class]) return;
|
||
|
||
_startDragging = YES;
|
||
self.menuView.userInteractionEnabled = NO;
|
||
}
|
||
|
||
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
|
||
if (![scrollView isKindOfClass:WMScrollView.class]) return;
|
||
|
||
self.menuView.userInteractionEnabled = YES;
|
||
_selectIndex = (int)(scrollView.contentOffset.x / _contentViewFrame.size.width);
|
||
self.currentViewController = self.displayVC[@(self.selectIndex)];
|
||
[self didEnterController:self.currentViewController atIndex:self.selectIndex];
|
||
[self.menuView deselectedItemsIfNeeded];
|
||
}
|
||
|
||
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView {
|
||
if (![scrollView isKindOfClass:WMScrollView.class]) return;
|
||
|
||
self.currentViewController = self.displayVC[@(self.selectIndex)];
|
||
[self didEnterController:self.currentViewController atIndex:self.selectIndex];
|
||
[self.menuView deselectedItemsIfNeeded];
|
||
}
|
||
|
||
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
|
||
if (![scrollView isKindOfClass:WMScrollView.class]) return;
|
||
|
||
if (!decelerate) {
|
||
self.menuView.userInteractionEnabled = YES;
|
||
CGFloat rate = _targetX / _contentViewFrame.size.width;
|
||
[self.menuView slideMenuAtProgress:rate];
|
||
[self.menuView deselectedItemsIfNeeded];
|
||
}
|
||
}
|
||
|
||
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
|
||
if (![scrollView isKindOfClass:WMScrollView.class]) return;
|
||
|
||
_targetX = targetContentOffset->x;
|
||
}
|
||
|
||
#pragma mark - WMMenuView Delegate
|
||
- (void)menuView:(WMMenuView *)menu didSelesctedIndex:(NSInteger)index currentIndex:(NSInteger)currentIndex {
|
||
if (!_hasInited) return;
|
||
_selectIndex = (int)index;
|
||
_startDragging = NO;
|
||
CGPoint targetP = CGPointMake(_contentViewFrame.size.width * index, 0);
|
||
[self.scrollView setContentOffset:targetP animated:self.pageAnimatable];
|
||
if (self.pageAnimatable) return;
|
||
// 由于不触发 -scrollViewDidScroll: 手动处理控制器
|
||
UIViewController *currentViewController = self.displayVC[@(currentIndex)];
|
||
if (currentViewController) {
|
||
[self wm_removeViewController:currentViewController atIndex:currentIndex];
|
||
}
|
||
[self wm_layoutChildViewControllers];
|
||
self.currentViewController = self.displayVC[@(self.selectIndex)];
|
||
|
||
[self didEnterController:self.currentViewController atIndex:index];
|
||
}
|
||
|
||
- (CGFloat)menuView:(WMMenuView *)menu widthForItemAtIndex:(NSInteger)index {
|
||
if (self.automaticallyCalculatesItemWidths) {
|
||
return [self wm_calculateItemWithAtIndex:index];
|
||
}
|
||
|
||
if (self.itemsWidths.count == self.childControllersCount) {
|
||
return [self.itemsWidths[index] floatValue];
|
||
}
|
||
return self.menuItemWidth;
|
||
}
|
||
|
||
- (CGFloat)menuView:(WMMenuView *)menu itemMarginAtIndex:(NSInteger)index {
|
||
if (self.itemsMargins.count == self.childControllersCount + 1) {
|
||
return [self.itemsMargins[index] floatValue];
|
||
}
|
||
return self.itemMargin;
|
||
}
|
||
|
||
- (CGFloat)menuView:(WMMenuView *)menu titleSizeForState:(WMMenuItemState)state atIndex:(NSInteger)index {
|
||
switch (state) {
|
||
case WMMenuItemStateSelected: return self.titleSizeSelected;
|
||
case WMMenuItemStateNormal: return self.titleSizeNormal;
|
||
}
|
||
}
|
||
|
||
- (UIColor *)menuView:(WMMenuView *)menu titleColorForState:(WMMenuItemState)state atIndex:(NSInteger)index {
|
||
switch (state) {
|
||
case WMMenuItemStateSelected: return self.titleColorSelected;
|
||
case WMMenuItemStateNormal: return self.titleColorNormal;
|
||
}
|
||
}
|
||
|
||
#pragma mark - WMMenuViewDataSource
|
||
- (NSInteger)numbersOfTitlesInMenuView:(WMMenuView *)menu {
|
||
return self.childControllersCount;
|
||
}
|
||
|
||
- (NSString *)menuView:(WMMenuView *)menu titleAtIndex:(NSInteger)index {
|
||
return [self titleAtIndex:index];
|
||
}
|
||
|
||
#pragma mark - WMPageControllerDataSource
|
||
- (CGRect)pageController:(WMPageController *)pageController preferredFrameForMenuView:(WMMenuView *)menuView {
|
||
NSAssert(0, @"[%@] MUST IMPLEMENT DATASOURCE METHOD `-pageController:preferredFrameForMenuView:`", [self.dataSource class]);
|
||
return CGRectZero;
|
||
}
|
||
|
||
- (CGRect)pageController:(WMPageController *)pageController preferredFrameForContentView:(WMScrollView *)contentView {
|
||
NSAssert(0, @"[%@] MUST IMPLEMENT DATASOURCE METHOD `-pageController:preferredFrameForContentView:`", [self.dataSource class]);
|
||
return CGRectZero;
|
||
}
|
||
|
||
@end
|