Coding源码学习第二部分(FunctionIntroManager.m)
接上篇。上篇有一个细节忘了写,在Coding_iOS-Info.plist 里面添加了一个key 是 Status bar is initially hidden Value 是 YES,在application 启动的时候隐藏状态栏,然后在
1 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 2 { 3 ...... 4 // 显示状态栏 5 [[UIApplication sharedApplication] setStatusBarHidden:NO withAnimation:UIStatusBarAnimationFade]; 6 ...... 7 }
本篇细读FunctionIntroManager 类,我认为该类的职责是Coding 为中秋节做的一个彩蛋。该类的唯一的一个类方法 + (void)showIntroPage; 只有在当前某个版本下的中秋节的的那天才不会直接return;
如果该类方法跳过return 向下执行,首先启动图会由EaseStartView 类换为一张中秋主题的Coding 图片, EaseStartView 类下篇会细读。FunctionIntroManage 类主要完成的功能是,提供一个类似首次启动引导页的功能,有几张图片组成调用 EAIntroView 这个第三方库,左右滑动可以展示几张Coding 风格的图片, 在最后一张图片左滑的时候进入[UIApplication sharedApplication].keyWindow.rootViewController 。除此之外Coding 还有调用 JazzHands 第三方库做的动画感十足的启动引导页,后面会做细读。首先看几张中秋风格的图片:
代码部分:
1 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 2 { 3 ...... 4 [self.window makeKeyAndVisible]; // 走到这里会把RootViewController 配置一遍 5 6 [FunctionIntroManager showIntroPage]; // 方法执行后直接return 了,并没有做什么事情,另外一个简单的启动引导页面,只在中秋节的时候启动用的。在指定的版本中有一个在中秋节期间的时候特别制作的启动页面 7 ...... 8 }
这里分析FunctionIntroManager 类之前,先对UIWindow 做一个延展阅读。
UIWindow 类
(一)UIWindow 介绍
1 NS_CLASS_AVAILABLE_IOS(2_0) @interface UIWindow : UIView
UIWindow是继承自UIView,之前感觉UIWindow 一直处于顶级, 以为UIView 继承自UIWindow,孰不知正好相反,通常在一个程序中只会有一个UIWindow,但可以手动创建多个UIWindow,同时加到程序里面。iOS程序启动完毕后,创建的第一个视图控件就是UIWindow,接着创建控制器和view,最后将控制器的view添加到UIWindow上,于是控制器的view就显示在屏幕上了,一个iOS程序之所以能显示到屏幕上,完全是因为它有UIWindow。也就说,没有UIWindow,就看不见任何UI界面。UIWindow是创建的第一个视图控件,创建的第一个对象是UIapplication。先创建UIwindow,再创建控制器,创建控制器的view,然后将控制器的view添加到UIWindow上。
在IOS应用中,我们使用UIWindow和UIView来呈现界面。UIWindow并不包含任何默认的内容,但它是被当做UIView的容器,用于放置应用中所有的UIView。从继承关系来看,UIWindow继承自UIView,所以UIWIndow除了具有UIView的所有功能外,还增加了一些特有的属性和方法。
通常在一个程序中只会有一个UIWindow,但有些时候我们调用系统的控件(例如UIAlertView)时,IOS系统为了保证UIAlertView在所有的界面之上,它会临时创建一个新的UIWindow,通过将其UIWindow的UIWindowLevel设置的更高,让UIWindow盖在所有的应用界面之上。(平时输入文字弹出的键盘,就处在一个新的UIWindow中)
(二)UIWindow 作用
1、作为容器,包含app 所要显示的所有视图,并展示app 的可视内容。
2、传递触摸消息到程序中view和其他对象,即把事件分发给视图以及其他对象。
3、与UIViewController协同工作,方便完成设备方向旋转的支持, 处理屏幕旋转。
(三)UIWindow 创建
在有storyboard的项目中,UIWindow是如何创建的?
为什么创建一个storyboard,没有看到创建uiwindow的过程?
它其实是把创建UIWindow的过程给屏蔽起来了。可以把代理的UIWindow的属性的值打印出来NSLog(@“window=%p”,self.window);打印出来确实是有值的,说明确实创建了UIWindow.不仅创建了UIWindow,默认还创建了UIWindow对应的控制器,也可以打印进行查看。NSLog(@“%@“,self.window.rootviewcontroller);
有storyboard的项目中的创建过程:
当用户点击应用程序图标的时候,先执行Main函数,执行UIApplicationMain(),根据其第三个和第四个参数创建Application,创建代理,并且把代理设置给application(看项目配置文件info.plist里面的storyboard的name,根据这个name找到对应的storyboard),开启一个事件循环,当程序加载完毕,他会调用代理的didFinishLaunchingWithOptions:方法。在调用didFinishLaunchingWithOptions:方法之前,会加载storyboard,在加载的时候创建一个window,接下来会创建箭头所指向的控制器,把该控制器设置为UIWindow的根控制器,接下来再将window显示出来,即看到了运行后显示的界面。(提示:关于这部分可以查看story的初始化的文档)
使用nib文件创建:
如果不使用storyboard,也可以用nib文件来代替。将一个window对象拖拽到Interface Builder文件中,并将这个文件指定为app的main interface。那么在app启动的时候,iOS也会自动创建window对象。
为了确保window的大小与屏幕大小吻合,需要在Interface Builder中对window对象勾选Full Screen at Launch这个属性。需要注意的是,window的尺寸永远应该是屏幕的尺寸,不应该考虑状态栏等元素,因为这些是view controller应该处理的问题。
在没有storyboard中的创建过程:
手写代码:
当然也可以通过手写代码的方式创建window。比如官方示例代码:
1 - (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 2 3 UIWindow *window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; 4 myViewController = [[MyViewController alloc] init]; 5 window.rootViewController = myViewController; 6 [window makeKeyAndVisible]; 7 8 return YES; 9 }
(四)UIWindowLevel & KeyWindow
1 UIKIT_EXTERN const UIWindowLevel UIWindowLevelNormal; 2 UIKIT_EXTERN const UIWindowLevel UIWindowLevelAlert; 3 UIKIT_EXTERN const UIWindowLevel UIWindowLevelStatusBar __TVOS_PROHIBITED;
什么是keyWindow,官方文档中是这样解释的"The key window is the one that is designated to receive keyboard and other non-touch related events. Only one window at a time may be the key window." 翻译过来就是说,keyWindow是指定的用来接收键盘以及非触摸类的消息,而且程序中每一个时刻只能有一个window是keyWindow。(非keyWindow也是可以接受键盘消息)
1 UIKIT_EXTERN NSString *const UIWindowDidBecomeVisibleNotification; // nil 2 UIKIT_EXTERN NSString *const UIWindowDidBecomeHiddenNotification; // nil 3 UIKIT_EXTERN NSString *const UIWindowDidBecomeKeyNotification; // nil 4 UIKIT_EXTERN NSString *const UIWindowDidResignKeyNotification; // nil
这四个通知对象中的object都代表当前已显示(隐藏),已变成keyWindow(非keyWindow)的window对象,其中的userInfo则是空的。于是我们可以注册这个四个消息,再打印信息来观察keyWindow的变化以及window的显示,隐藏的变动。
如果我们创建的UIWindow需要处理键盘事件,那就要合理的将其设置为keyWindow。keyWindow是被系统设计用来接受键盘和其他非触摸事件的UIWindow。我们可以通过makeKeyWindow 和 resignKeyWindow 方法来将自己创建的UIWindow实例设置成keyWindow。
Action Sheet和Alert View
知道了window的存在之后,感觉也能知道很多事情。
比如,iOS中的UIActionSheet和UIAlertView其实是显示在另一个window上的。
监听UIWindowDidResignKeyNotification
,可以发现,当action sheet弹出时,UIWindowDidResignKeyNotification
通知被发送了。此时检查app所在的window,发现它已经不再是key window了。
(五)把View 添加到UIWindow
1、addSubview
直接将view通过addSubview方式添加到window中,程序负责维护view的生命周期以及刷新,但是并不会为去理会view对应的ViewController,因此采用这种方法将view添加到window以后,我们还要保持view对应的ViewController的有效性,不能过早释放。
2、rootViewController
rootViewController时UIWindow的一个遍历方法,通过设置该属性为要添加view对应的ViewController,UIWindow将会自动将其view添加到当前window中,同时负责ViewController和view的生命周期的维护,防止其过早释放
两个方法的区别:
以后的开发中,建议使用(2).因为方法(1)存在一些问题,比如说控制器上面可能由按钮,需要监听按钮的点击事件,如果是1,那么按钮的事件应该由控制器来进行管理。但控制器是一个局部变量,控制器此时已经不存在了,但是控制器的view还在,此时有可能会报错。注意:方法执行完,这个控制器就已经不存在了。
问题描述1:当view发生一些事件的时候,通知控制器,但是控制器已经销毁了,所以可能出现未知的错误。
问题描述2:添加一个开关按钮,让屏幕360度旋转(两者的效果不一样)。当发生屏幕旋转事件的时候,UIapplication对象会将旋转事件传递给uiwindow,uiwindow又会将旋转事件传递给它的根控制器,由根控制器决定是否需要旋转
UIapplication->uiwindow->根控制器(第一种方式没有根控制器,所以不能跟着旋转)。
提示:不通过控制器的view也可以做开发,但是在实际开发中,不要这么做,不要直接把view添加到UIWindow上面去。因为,难以管理。
(六)UIWindow 获取
1.主窗口和次窗口
[self.window makekeyandvisible]; // 让窗口成为主窗口,并且显示出来。有这个方法,才能把信息显示到屏幕上。
因为Window有makekeyandvisible这个方法,可以让这个Window凭空的显示出来,而其他的view没有这个方法,所以它只能依赖于Window,Window显示出来后,view才依附在Window上显示出来。
[self.window make keywindow]; // 让uiwindow成为主窗口,但不显示。
2.获取UIwindow
(1)[UIApplication sharedApplication].windows 在本应用中打开的UIWindow列表,这样就可以接触应用中的任何一个UIView对象(平时输入文字弹出的键盘,就处在一个新的UIWindow中)
(2)[UIApplication sharedApplication].keyWindow(获取应用程序的主窗口)用来接收键盘以及非触摸类的消息事件的UIWindow,而且程序中每个时刻只能有一个UIWindow是keyWindow。
提示:如果某个UIWindow内部的文本框不能输入文字,可能是因为这个UIWindow不是keyWindow
(3)view.window获得某个UIView所在的UIWindow
参考链接:
接下来接着学习FunctionIntroManager 类:
+ (void)showIntroPage{}, 方法内部,首先判断需不需要展示这个特殊的引导页:
1 + (BOOL)needToShowIntro{ 2 // return YES; 3 NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; 4 NSString *preVersion = [defaults stringForKey:kIntroPageKey]; 5 BOOL needToShow = ![preVersion isEqualToString:kVersion_Coding]; // 指定版本,特定的版本才有这个特定的图片引导启动 6 if (![NSDate isDuringMidAutumn]) {//中秋节期间才显示 7 needToShow = NO; 8 } 9 return needToShow; 10 }
preVersion 是取本地plist 保存的系统版本信息和当前的系统版本比较,获取当前系统版本的方法:
1 //版本号 2 #define kVersion_Coding [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"] 3 #define kVersionBuild_Coding [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]
[NSDate isDuringMidAutumn]; 判断当前是不是中秋节,对NSDate 添加了两个category ,NSDate+Common.m 和 NSDate+convenience.m:
1 + (BOOL)isDuringMidAutumn{ 2 // return YES; 3 BOOL isDuringMidAutumn; 4 NSDate *curDate = [NSDate date]; 5 if (curDate.year != 2015 || 6 curDate.month != 9 || 7 curDate.day < 25 || 8 curDate.day > 27) {//中秋节期间才显示 9 isDuringMidAutumn = NO; 10 }else{ 11 isDuringMidAutumn = YES; 12 } 13 return isDuringMidAutumn; 14 }
curDate.year、curDate.month、curDate.day:
1 -(int)year { 2 NSCalendar *gregorian = [[NSCalendar alloc] 3 initWithCalendarIdentifier:NSGregorianCalendar]; 4 NSDateComponents *components = [gregorian components:NSYearCalendarUnit fromDate:self]; 5 return (int)[components year]; 6 } 7 8 9 -(int)month { 10 NSCalendar *gregorian = [[NSCalendar alloc] 11 initWithCalendarIdentifier:NSGregorianCalendar]; 12 NSDateComponents *components = [gregorian components:NSMonthCalendarUnit fromDate:self]; 13 return (int)[components month]; 14 } 15 16 -(int)day { 17 NSCalendar *gregorian = [[NSCalendar alloc] 18 initWithCalendarIdentifier:NSGregorianCalendar]; 19 NSDateComponents *components = [gregorian components:NSDayCalendarUnit fromDate:self]; 20 return (int)[components day]; 21 }
这三个实例方法是获取当前的年月日,引出了NSCalendar 类 和 NSDateComponents 类,这里做一个延展阅读:
NSCalendar + NSDateComponents + NSDateFomatter + NSDate
历法能使人类确定每一日在无限的时间中的确切位置并记录历史。日历,历法,一般历法都是遵循固定的规则的,具有周期性。日历都是已知的或可预测的。任何一种具体的历法,首先必须明确规定起始点,即开始计算的年代,这叫“纪元”;以及规定一年的开端,这叫“岁首”。此外,还要规定每年所含的日数,如何划分月份,每月有多少天等等。
NSCalendar 对世界上现存的常用的历法进行了封装,既提供了不同历法的时间信息,又支持日历的计算。NSDateFomatter 表示的时间默认以公历为参考,可以通过设置calendar 属性变量获得特定历法下的时间表示。NSDate 是独立与任何历法的,它只是时间相对于某个时间点的时间差,NSDate 是进行日历计算的基础。NSDateComponents将时间表示成适合人类阅读和使用的方式,通过NSDateComponents可以快速而简单地获取某个时间点对应的“年”,“月”,“日”,“时”,“分”,“秒”,“周”等信息。当然一旦涉及了年月日时分秒就要和某个历法绑定,因此NSDateComponents必须和NSCalendar一起使用,默认为公历。NSDateComponents除了像上面说的表示一个时间点外,还可以表示时间段,例如:两周,三个月,20年,7天,10分钟,50秒等等。时间段用于日历的计算,例如:获取当前历法下,三个月前的某个时间点。可以说,要获取某个时间点在某个历法下的表示,需要NSDateComponents;要计算当前时间点在某个历法下对应的一个时间段前或后的时间点,需要NSDateComponents。NSDateComponents返回的day, week, weekday, month, year这一类数据都是从1开始的。因为日历是给人看的,不是给计算机看的,从0开始就是个错误。
当前所有的历法类型:
FOUNDATION_EXPORT NSString * const NSCalendarIdentifierGregorian NS_AVAILABLE(10_6, 4_0); // the common calendar in Europe, the Western Hemisphere, and elsewhere // 公历 FOUNDATION_EXPORT NSString * const NSCalendarIdentifierBuddhist NS_AVAILABLE(10_6, 4_0); // 佛教日历 FOUNDATION_EXPORT NSString * const NSCalendarIdentifierChinese NS_AVAILABLE(10_6, 4_0); // 中国农历 FOUNDATION_EXPORT NSString * const NSCalendarIdentifierCoptic NS_AVAILABLE(10_6, 4_0); // 埃及日历 FOUNDATION_EXPORT NSString * const NSCalendarIdentifierEthiopicAmeteMihret NS_AVAILABLE(10_6, 4_0); FOUNDATION_EXPORT NSString * const NSCalendarIdentifierEthiopicAmeteAlem NS_AVAILABLE(10_6, 4_0); FOUNDATION_EXPORT NSString * const NSCalendarIdentifierHebrew NS_AVAILABLE(10_6, 4_0); // 希伯来日历 FOUNDATION_EXPORT NSString * const NSCalendarIdentifierISO8601 NS_AVAILABLE(10_6, 4_0); // ISO8601 FOUNDATION_EXPORT NSString * const NSCalendarIdentifierIndian NS_AVAILABLE(10_6, 4_0); // 印度日历 FOUNDATION_EXPORT NSString * const NSCalendarIdentifierIslamic NS_AVAILABLE(10_6, 4_0); // 伊斯兰日历 FOUNDATION_EXPORT NSString * const NSCalendarIdentifierIslamicCivil NS_AVAILABLE(10_6, 4_0); // 伊斯兰教日历 FOUNDATION_EXPORT NSString * const NSCalendarIdentifierJapanese NS_AVAILABLE(10_6, 4_0); // 日本日历 FOUNDATION_EXPORT NSString * const NSCalendarIdentifierPersian NS_AVAILABLE(10_6, 4_0); FOUNDATION_EXPORT NSString * const NSCalendarIdentifierRepublicOfChina NS_AVAILABLE(10_6, 4_0); // A simple tabular Islamic calendar using the astronomical/Thursday epoch of CE 622 July 15 FOUNDATION_EXPORT NSString * const NSCalendarIdentifierIslamicTabular NS_AVAILABLE(10_10, 8_0); // The Islamic Umm al-Qura calendar used in Saudi Arabia. This is based on astronomical calculation, instead of tabular behavior. FOUNDATION_EXPORT NSString * const NSCalendarIdentifierIslamicUmmAlQura NS_AVAILABLE(10_10, 8_0);
NSDateComponents 实例化的方式
第一种:
1 // 定义一个遵循某个历法的日历对象 2 NSCalendar *greCalendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian]; 3 // 通过已定义的日历对象,获取某个时间点的NSDateComponents 表示,并设置需要设置哪些信息 4 NSDateComponents *dateComponents = [greCalendar components:NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay | NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond | NSCalendarUnitWeekday | NSCalendarUnitWeekdayOrdinal | NSCalendarUnitEra | NSCalendarUnitQuarter | NSCalendarUnitWeekOfMonth | NSCalendarUnitWeekOfYear | NSCalendarUnitYearForWeekOfYear | NSCalendarUnitNanosecond | NSCalendarUnitCalendar | NSCalendarUnitTimeZone fromDate:[NSDate date]]; 5 NSLog(@"year(年份): %li", (long)dateComponents.year); 6 NSLog(@"quarter(季度): %li", (long)dateComponents.quarter); 7 NSLog(@"month(月份)%li", (long)dateComponents.month); 8 NSLog(@"day(日期)%li", (long)dateComponents.day); 9 NSLog(@"hour(小时)%li", (long)dateComponents.hour); 10 NSLog(@"minute(分钟)%li", (long)dateComponents.minute); 11 NSLog(@"second(秒)%li", (long)dateComponents.second); 12 // 周日 1 周一 2 周二 3 ... 13 NSLog(@"weekday(星期)%li", (long)dateComponents.weekday); 14 NSLog(@"weekOfYear(该年第几周)%li", (long)dateComponents.weekOfYear); 15 NSLog(@"weekOfMonth(该月第几周)%li", (long)dateComponents.weekOfMonth);
注意:若获取dateComponents对象时,设置components的时候未添加
NSCalendarUnitYear ,dateComponents.year将返回错误的数值,其他的也一样,所以使用NSDateComponents表示时间时,要确保需要使用的数据都在componets中添加了。
第二种:
1 // 先定义一个遵循某个历法的日历对象 2 NSCalendar *greCalendarTwo = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian]; 3 // 定义一个NSDateComponents 对象,设置一个时间点 4 NSDateComponents *dateComponentsForDate = [[NSDateComponents alloc] init]; 5 [dateComponentsForDate setDay:22]; 6 [dateComponentsForDate setMonth:12]; 7 [dateComponentsForDate setYear:1992]; 8 // 根据设置的dateComponentsForDate 获取历法中与之对应的时间点 9 // 这里的时分秒会使用NSDateComponents 中规定的默认值,一般是0 或 1 10 NSDate *dateFromDateComponentsForDate = [greCalendarTwo dateFromComponents:dateComponentsForDate]; 11 // 定义一个NSDateComponents 对象, 设置一个时间段 12 NSDateComponents *dateComponentsAsTimeQantum = [[NSDateComponents alloc] init]; 13 [dateComponentsForDate setDay:6]; 14 // 在当前历法下,获取6 天后的时间点 15 NSDate *dateFromDateComponentsAsTimeQantum = [greCalendarTwo dateByAddingComponents:dateComponentsAsTimeQantum toDate:[NSDate date] options:NSCalendarWrapComponents];
第三种:
1 // 先定义一个尊徐某个历法的日历对象 2 NSCalendar *greCalendarThree = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian]; 3 // 根据两个时间点,定义NSDatecomponents 对象,从而获取这两个时间点的时间差 4 NSDateComponents *dateComponentsThree = [greCalendarThree components:NSCalendarUnitYear fromDate:[NSDate dateWithTimeIntervalSince1970:0] toDate:[NSDate date] options:NSCalendarWrapComponents]; 5 NSLog(@"number of years: %li", (long)dateComponentsThree.year);
NSCalendar中比较重要的方法和概念
(1) firstWeekday是大家比较容易浑淆的东西。
大家在使用dateComponents.weekday获取某天对应的星期时,会发现,星期日对应的值为1,星期一对应的值为2,星期二对应的值为3,依次递推,星期六对应的值为7,这与我们平时理解的方式不一样。然后,我们就开始找是不是可以设置这种对应关系。终于,我们在NSCalendar中发现了firstWeekday这个变量,从字面意思上看貌似就是我们寻找的那个东西。可是,设置过firstWeekday后,我们又发现完全没有作用,真是郁闷啊!其实,大家不必郁闷,因为郁闷也没用,iOS中规定的就是周日为1,周一为2,周二为3,周三为4,周四为5,周五为6,周六为7,无法通过某个设置改变这个事实的,只能在使用的时候注意一下这个规则了。那firstWeekday是干什么用的呢?大家设置一下firstWeekday,再获取一下dateComponents.weekOfYear或dateComponents.weekOfMonth,看看返回的数据是否发生了变化。firstWeekday的作用确实是修改当前历法中周的起始位置,但是不能修改周日对应的数值,只能修改一年或一个月中周的数量,以及周的次序。
(2)
1 - (NSRange)rangeOfUnit:(NSCalendarUnit)smaller inUnit:(NSCalendarUnit)larger forDate:(NSDate *)date;
Unit: 单元 NSRange : typedef struct _NSRange{NSUInteger location; NSUInteger length;} NSRange;
我们大致可以理解为:某个时间点所在的“小单元”,在“大单元”中的数量(返回值range的location属性变量的值一般是错误的)。例如:
1 // 当前时间对应的月份中有几天 2 [[NSCalendar currentCalendar] rangeOfUnit:NSCalendarUnitDay inUnit:NSCalendarUnitMonth forDate:[NSDate date]].length; 3 // 当前时间对应的月份中有几周(前面的firstWeekday 会影响这个结果) 4 [[NSCalendar currentCalendar] rangeOfUnit:NSCalendarUnitWeekday inUnit:NSCalendarUnitMonth forDate:[NSDate date]].length;
(3)
1 - (NSUInteger)ordinalityOfUnit:(NSCalendarUnit)smaller inUnit:(NSCalendarUnit)larger forDate:(NSDate *)date;
我们大致可以理解为:某个时间点所在的“小单元”,在“大单元”中的位置(从1开始)。例如:
1 // 当前时间对应的周是当前年中的第几周 2 [[NSCalendar currentCalendar] ordinalityOfUnit:NSCalendarUnitWeekOfYear inUnit:NSCalendarUnitYear forDate:[NSDate date]]; 3 [[NSCalendar currentCalendar] ordinalityOfUnit:NSCalendarUnitWeekday inUnit:NSCalendarUnitYear forDate:[NSDate date]]; 4 // 当前时间对应的周是当前月中的第几周 5 [[NSCalendar currentCalendar] ordinalityOfUnit:NSCalendarUnitWeekOfMonth inUnit:NSCalendarUnitYear forDate:[NSDate date]]; 6 [[NSCalendar currentCalendar] ordinalityOfUnit:NSCalendarUnitWeekday inUnit:NSCalendarUnitMonth forDate:[NSDate date]];
在这里:NSCalendarUnitWeekOfYear ,NSCalendarUnitWeekOfMonth 与NSCalendarUnitWeekday 的使用结果相同, 为了避免混淆,建议此处使用
NSCalendarUnitWeekday , 而定义NSDateComponents 时使用 NSCalendarUnitWeekOfMonth NSCalendarUnitWeekOfMonth。
(4)
1 - (BOOL)rangeOfUnit:(NSCalendarUnit)unit startDate:(NSDate * __nullable * __nullable)datep interval:(nullable NSTimeInterval *)tip forDate:(NSDate *)date NS_AVAILABLE(10_5, 2_0);
大致可以理解为:"某个时间点"所在的"单元"的起始时间,以及起始时间距离"某个时间点" 的时差(单位秒)。例如:
1 NSDate *startDateOfYear; 2 NSDate *startDateOfMonth; 3 NSDate *startDateOfWeek; 4 NSDate *startDateOfDay; 5 NSTimeInterval TIOfYear; 6 NSTimeInterval TIOfMonth; 7 NSTimeInterval TIOfWeek; 8 NSTimeInterval TIOfDay; 9 [[NSCalendar currentCalendar] rangeOfUnit:NSCalendarUnitYear startDate:&startDateOfYear interval:&TIOfYear forDate:[NSDate date]]; 10 [[NSCalendar currentCalendar] rangeOfUnit:NSCalendarUnitMonth startDate:&startDateOfMonth interval:&TIOfMonth forDate:[NSDate date]]; 11 [[NSCalendar currentCalendar] rangeOfUnit:NSCalendarUnitWeekday startDate:&startDateOfWeek interval:&TIOfWeek forDate:[NSDate date]]; 12 [[NSCalendar currentCalendar] rangeOfUnit:NSCalendarUnitDay startDate:&startDateOfDay interval:&TIOfDay forDate:[NSDate date]]; 13 NSLog(@"firstDateOfYear: %@, FirstDateOfMonth: %@, FirstDateOfWeek: %@, FirstDateOfDay: %@", startDateOfYear, startDateOfMonth, startDateOfWeek, startDateOfDay); 14 NSLog(@"TIOfYear: %f, TIOfMonth: %f, TIOfWeek: %f, TIOfDay: %f", TIOfYear, TIOfMonth, TIOfWeek, TIOfDay);
1 // + currentCalendar 2 // 取得当前用户的逻辑日历 3 // currentCalendar 取得的值会一直保持在cache 中,第一个取得以后如果用户修改该系统日历设定,这个值也不会改变 4 NSCalendar *calendar = [NSCalendar currentCalendar]; 5 NSLog(@"calendar = %@", calendar); 6 // + (id)autoupdatingCurrentCalendar 7 // 取得当前用户的逻辑日历 8 // 用autoupdatingCurrentCalendar ,那么每次取得的值都会是当前系统的日历的值 9 NSCalendar *autoupdatingCurrent = [NSCalendar autoupdatingCurrentCalendar]; 10 NSLog(@"autoupdatingCurrent = %@", autoupdatingCurrent); 11 // Initializing a Calendar 12 // - initWithCalendarIdentifier: 13 // 如果想要用公历的时候,就要将NSDateFormatter 的日历设置成公历。 否则随着用户的系统设定的改变,取得的日期的格式也会不一样 14 NSCalendar *initCalendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian]; 15 NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; 16 [formatter setCalendar:initCalendar]; 17 // - setFirstWeekday; 18 // 设置第一个工作日 19 // 设定每周的第一天从星期几开始,比如 20 // 如果设定从星期日开始, 则value 传入 1 21 // 如果设定从星期一开始, 则value 传入 2 22 // 以此类推 23 [initCalendar setFirstWeekday:2]; 24 // - setLocale; 25 // 设置区域 26 [initCalendar setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"zh"]]; 27 // 设定作为(每年及每月)第一周必须包含的最少天数,比如: 如需设定第一周最少包括7 天, 则value 传入 7 28 // - setMinimumDaysInFirstWeek 29 [initCalendar setMinimumDaysInFirstWeek:7]; 30 // - setTimeZone 31 // 设置时区 32 [initCalendar setTimeZone:[NSTimeZone defaultTimeZone]]; 33 // -- Getting Information About a calendar 34 // - calendarIdentifier 35 // 返回日历的标识符 36 NSString *calendarIdentifier = [initCalendar calendarIdentifier]; 37 NSLog(@"calendarIdentifier = %@", calendarIdentifier); 38 // - firstWeekday 39 // 返回日历指定的每周的第一条从星期几开始。缺省为星期天,即firstWeek = 1 40 NSUInteger firstWeekday = [initCalendar firstWeekday]; 41 NSLog(@"firstWeekDay = %lu", (unsigned long)firstWeekday); 42 // - locale 43 // 返回日历指定的地区信息 44 NSLocale *locale = [initCalendar locale]; 45 NSLog(@"locale = %@", locale.localeIdentifier); 46 // - maximumRangeOfUnit: // 返回单元的最大范围 47 // - minimumRangeOfUnit: // 返回单元的最小范围 // 比如 Day Calendar Unit 就是一个月最多31 天这个意思 48 NSRange range = [initCalendar maximumRangeOfUnit:NSCalendarUnitDay]; 49 NSLog(@"range = %lu", (unsigned long)range.length); 50 // - minimumDaysInFirstWeek 51 // 返回日历指定的第一周必须包含的最少天数 52 NSUInteger minimumDays = [initCalendar minimumDaysInFirstWeek]; 53 NSLog(@"minimumDays = %lu", (unsigned long)minimumDays); 54 // - ordinalityOfUnit: inUmit:forDate: 55 // 在一个给定的时间,小日历单元如(一天)在大日历单元(一周)中的序数 56 // 比如forDate 参数是星期一,而且firstWeekday 参数被设置为2 (也就是星期一为一周的第一天),那么返回为1 57 // 通过这个函数可以判断 例如: 给定的日期是在一周的第几天,或一月的第几周。一年的第几个月。一年的第几天等 58 // 注:firstWeekday 的设定会影响这个函数的返回值 59 NSUInteger ordinality = [initCalendar ordinalityOfUnit:NSCalendarUnitWeekday inUnit:NSCalendarUnitWeekday forDate:[NSDate date]]; 60 NSLog(@"ordinality = %lu", (unsigned long)ordinality); 61 //- rangeOfUnit:inUnit:forDate: 62 //一个小日历单元下。大日历单元的范围 例如 小日历单元是天。大日历单元是周。那么范围就是7天。就是1-7 63 NSRange rangeOfUnit = [initCalendar rangeOfUnit:NSCalendarUnitWeekday inUnit:kCFCalendarUnitWeek forDate:[NSDate date]]; 64 NSLog(@"rangeOfUnit = %lu",(unsigned long)rangeOfUnit.length); 65 //- timeZone: 66 //返回日历指定的时区信息。 67 NSTimeZone *timeZone = [initCalendar timeZone]; 68 NSLog(@"timeZone = %@",timeZone.abbreviation); 69 70 //-- 71 72 //-- Calendrical Calculations 73 74 //- components:fromDate: 75 //返回时间组件 76 unsigned unitFlags = NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay; 77 NSDateComponents *comps = [initCalendar components:unitFlags fromDate:[NSDate date]]; 78 NSLog(@"NSDateComponents - %ld",(long)comps.year); 79 80 //- components:fromDate:toDate:options: 81 //返回时间组件 比较2个日期 82 NSDate *startDate = [NSDate dateWithTimeIntervalSince1970:0]; 83 NSDate *endDate = [NSDate date]; 84 unsigned int unitFlags2 = NSCalendarUnitMonth | NSCalendarUnitDay; 85 NSDateComponents *comps2 = [initCalendar components:unitFlags2 fromDate:startDate toDate:endDate options:0]; 86 NSInteger months = [comps2 month]; 87 NSInteger days = [comps2 day]; 88 NSLog(@"months = %ld days = %ld",(long)months,(long)days); 89 90 //- dateByAddingComponents:toDate:options: 91 //追加日期 并返回一个新日期 92 // 93 NSDate *currentDate = [NSDate date]; 94 NSDateComponents *comps3 = [[NSDateComponents alloc] init]; 95 [comps3 setMonth:2]; 96 [comps3 setDay:3]; 97 NSDate *newDate = [initCalendar dateByAddingComponents:comps3 toDate:currentDate options:0]; 98 NSLog(@"newDate = %@",newDate); 99 100 //- dateFromComponents: 101 //创建日期 102 { 103 NSCalendar *calendar = [NSCalendar currentCalendar]; 104 NSDateComponents *comps = [[NSDateComponents alloc] init]; 105 [comps setYear:1965]; 106 [comps setMonth:1]; 107 [comps setDay:1]; 108 [comps setHour:2]; 109 [comps setMinute:10]; 110 [comps setSecond:0]; 111 NSDate *date = [calendar dateFromComponents:comps]; 112 NSLog(@"date = %@",date); 113 }
参考链接:
http://www.cnblogs.com/wujian1360/archive/2011/09/05/2168007.html
http://www.cnblogs.com/CCSSPP/archive/2013/07/11/3183410.html
http://my.oschina.net/yongbin45/blog/156181
接下来继续看FunctionIntroManager 类:
假设当前是指定版本且今天是中秋节,方法不return ,继续往下执行。
下面介绍一下 EAIntroView 这个第三方库, github地址:https://github.com/ealeksandrov/EAIntroView
EAIntroView 是一个用来实现软件启动时介绍的控件,支持多个视图进行滑动显示。
可灵活自定义的App介绍界面,使用简单。特色包括:
1. 滑动到最后一页,继续滑动将隐藏介绍页,进入App页面;当然也可以在任意一页点击“Skip”按钮直接跳进App页面;
2. 介绍页面之间的滑动切换采用淡入淡出的效果(cross-dissolve transition);
3. 可以任意设置每页的元素,包括背景图片、标题、标题图片、描述以及这些元素的位置;
4. 支持storyboard/IB。
基本使用方式是: 创建一组EAIntropage(可自定义,具体使用见下文),使用这组EAIntropage 创建一个EAIntroView的视图IntroView,将这个IntroView showInView到想要展示的视图上(见下文)
-
pageWithCustomView://自定义视图
-
pageWithCustomViewFromNibNamed://自定义nib
代理协议:
-
introDidFinish: //完成引导
-
intro:pageAppeared:withIndex: //引导页切换
IntroView支持的方法:
-
setPages://设置界面
-
showInView:animateDuration://设置展示动画
-
hideWithFadeOutDuration://显示和消失时间
-
setCurrentPageIndex: //设置当前显示的界面以及动画
第一步: 创建界面
每一个界面需要通过[EAIntroPage page]来创建,你可以自定义属性,所有的属性都是可选的.或者你可以通过你自定义的view(可以是nib),使用这种方式大多数选项就被忽略了.例如:
1 // 基本的创建方式 2 EAIntroPage *page1 = [EAIntroPage page]; 3 page1.title = @"Hello world"; 4 page1.desc = sampleDescription1; 5 // 自定义的,这些属性都是可选的 6 EAIntroPage *page2 = [EAIntroPage page]; 7 page2.title = @"This is page 2"; 8 page2.titleFont = [UIFont fontWithName:@"Georgia-BoldItalic" size:20]; 9 page2.titlePositionY = 220; 10 page2.desc = sampleDescription2; 11 page2.descFont = [UIFont fontWithName:@"Georgia-Italic" size:18]; 12 page2.descPositionY = 200; 13 page2.titleIconView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"title2"]]; 14 page2.titleIconPositionY = 100; 15 //nib的自定义视图 16 EAIntroPage *page3 = [EAIntroPage pageWithCustomViewFromNibNamed:@"IntroPage"]; 17 page3.bgImage = [UIImage imageNamed:@"bg2"];
第二步:创建介绍视图
所有的页面创建完成后,创建介绍视图,只是在介绍视图中按顺序展示.也可以通过传递给IntroView一组视图初始化, IntroView将重建视图的内容.
1 2 EAIntroView *intro = [[EAIntroView alloc] initWithFrame:self.view.bounds andPages:@[page1,page2,page3,page4]]; 3 4 5 //设置代理 6 [intro setDelegate:self];
第三步: 展示引导图
1 [intro showInView:self.view animateDuration:0.0];
在 FunctionIntroManager 是:
1 NSMutableArray *pages = [NSMutableArray new]; 2 for (int index = 0; index < kIntroPageNum; index ++) { 3 EAIntroPage *page = [self p_pageWithIndex:index]; // 创建EAIntroPage 4 [pages addObject:page]; 5 } 6 if (pages.count <= 0) { 7 return; 8 } 9 EAIntroView *introView = [[EAIntroView alloc] initWithFrame:kScreen_Bounds andPages:pages]; 10 introView.backgroundColor = [UIColor whiteColor]; 11 introView.swipeToExit = YES; 12 introView.scrollView.bounces = YES; 13 14 // 这里使用了 introView.swipeToExit = YES, 在滑动到pageController 最后一页的时候再向左滑即退出,进入app 内部 15 16 // introView.skipButton = [self p_skipButton]; 17 // introView.skipButtonY = 20.f + CGRectGetHeight(introView.skipButton.frame); 18 // introView.skipButtonAlignment = EAViewAlignmentCenter; 19 20 if (pages.count <= 1) { 21 introView.pageControl.hidden = YES; 22 }else{ 23 introView.pageControl = [self p_pageControl]; // 创建SMPageController 24 introView.pageControlY = 10.f + CGRectGetHeight(introView.pageControl.frame); 25 } 26 [introView showFullscreen]; 27 // 保存启动页的版本信息 intro_page_version 28 [self markHasBeenShowed];
#define kIntroPageNum 2
for 循环创建2 个EAIntroPage 添加到 pages 数组里面,然后创建EAIntroView 把pages 初始化时传递给他。
然后下面根据EAIntroPage 个数判断是否显示introView.pageControl .当需要pageControl 时,调用SMPageControl 这个第三方库创建pageControl;
最后[self markHasBeenShowed]; 把版本信息写进本地。
这里主要看一下EAIntroView 的展示函数:
1 - (void)showFullscreen { 2 [self showFullscreenWithAnimateDuration:0.3f andInitialPageIndex:0]; // 展示在整个屏幕上 默认的动画时间是 0.3 秒,透明度由0 变到1 3 } 4 5 - (void)showFullscreenWithAnimateDuration:(CGFloat)duration { 6 [self showFullscreenWithAnimateDuration:duration andInitialPageIndex:0]; // 设置的动画的时间, 并且默认的是从第 0 页开始, 也就是从第一页展示引导页 7 } 8 9 - (void)showFullscreenWithAnimateDuration:(CGFloat)duration andInitialPageIndex:(NSUInteger)initialPageIndex { 10 UIView *selectedView; 11 12 NSEnumerator *frontToBackWindows = [UIApplication.sharedApplication.windows reverseObjectEnumerator]; // 按照索引号从大到小访问数组的元素,而不是从小到大访问数组的元素,即逆序遍历数组。下面会做一个延伸阅读。 13 for (UIWindow *window in frontToBackWindows) { // 从windows 数组里面获取application 的当前的Window 14 BOOL windowOnMainScreen = window.screen == UIScreen.mainScreen; 15 BOOL windowIsVisible = !window.hidden && window.alpha > 0; 16 BOOL windowLevelNormal = window.windowLevel == UIWindowLevelNormal; 17 18 if (windowOnMainScreen && windowIsVisible && windowLevelNormal) { 19 selectedView = window; 20 break; 21 } 22 } 23 24 [self showInView:selectedView animateDuration:duration withInitialPageIndex:initialPageIndex]; // 在 selectedView 上展示 25 } 26 27 - (void)showInView:(UIView *)view { 28 [self showInView:view animateDuration:0.3f withInitialPageIndex:0]; // 在指定的View 上展示 29 } 30 31 - (void)showInView:(UIView *)view animateDuration:(CGFloat)duration { 32 [self showInView:view animateDuration:duration withInitialPageIndex:0]; 33 } 34 35 - (void)showInView:(UIView *)view animateDuration:(CGFloat)duration withInitialPageIndex:(NSUInteger)initialPageIndex { 36 if(![self pageForIndex:initialPageIndex]) { // 如果指定的首先展示的页的页数大于总页数,直接return 37 NSLog(@"Wrong initialPageIndex received: %ld",(long)initialPageIndex); 38 return; 39 } 40 41 self.currentPageIndex = initialPageIndex; 42 self.alpha = 0; 43 44 if(self.superview != view) { // 如果不在指定的View 上则添加 45 [view addSubview:self]; 46 } else { 47 [view bringSubviewToFront:self]; // 如果self 在指定的View 上,则把self 提到View 最前面 48 } 49 50 [UIView animateWithDuration:duration animations:^{ // 在duration 内透明度变成 1 51 self.alpha = 1; 52 } completion:^(BOOL finished) { 53 EAIntroPage *currentPage = _pages[self.currentPageIndex]; 54 if(currentPage.onPageDidAppear) currentPage.onPageDidAppear(); // EAIntroPage 页面显示完毕的block ,当EAIntroPage 显示完毕执行onPageDidAppear block 55 56 if ([(id)self.delegate respondsToSelector:@selector(intro:pageAppeared:withIndex:)]) { // 这行这个delegate 都是在首次显示完毕的时候执行 代理和block 同时存在,可以自由选择 57 [self.delegate intro:self pageAppeared:_pages[self.currentPageIndex] withIndex:self.currentPageIndex]; 58 } 59 }]; 60 }
另外在创建EAIntroPage 的时候用到了三个判断当前手机型号的宏,以此选择不同尺寸的启动图片。
1 #define kDevice_Is_iPhone5 ([UIScreen instancesRespondToSelector:@selector(currentMode)] ? CGSizeEqualToSize(CGSizeMake(640, 1136), [[UIScreen mainScreen] currentMode].size) : NO) 2 #define kDevice_Is_iPhone6 ([UIScreen instancesRespondToSelector:@selector(currentMode)] ? CGSizeEqualToSize(CGSizeMake(750, 1334), [[UIScreen mainScreen] currentMode].size) : NO) 3 #define kDevice_Is_iPhone6Plus ([UIScreen instancesRespondToSelector:@selector(currentMode)] ? CGSizeEqualToSize(CGSizeMake(1242, 2208), [[UIScreen mainScreen] currentMode].size) : NO)
UIPageController 部分是在EAIntroView 内部根据偏移量做self.pageControl.currentPage 和 当前的EAIntroPage 的改变。
以下代码是做每次切换EAIntroPage 时的淡入淡出:
1 - (void)makePanelVisibleAtIndex:(NSUInteger)panelIndex{ 2 [UIView animateWithDuration:0.3 animations:^{ 3 for (int idx = 0; idx < _pages.count; idx++) { 4 if (idx == panelIndex) { 5 [[self viewForPageIndex:idx] setAlpha:[self alphaForPageIndex:idx]]; 6 } else { 7 if(!self.hideOffscreenPages) { 8 [[self viewForPageIndex:idx] setAlpha:0]; 9 } 10 } 11 } 12 }]; 13 }
最后一页向左滑动退出引导的处理:
1 - (void)finishIntroductionAndRemoveSelf { 2 if ([(id)self.delegate respondsToSelector:@selector(introDidFinish:)]) { // 启动图引导完毕的代理 3 [self.delegate introDidFinish:self]; 4 } 5 6 // Remove observer for rotation 7 [[NSNotificationCenter defaultCenter] removeObserver:self]; // 移除通知 8 [[UIDevice currentDevice] endGeneratingDeviceOrientationNotifications]; // 结束设备转向通知 9 10 //prevent last page flicker on disappearing 11 self.alpha = 0; 12 13 //Calling removeFromSuperview from scrollViewDidEndDecelerating: method leads to crash on iOS versions < 7.0. 14 //removeFromSuperview should be called after a delay 15 dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)0); 16 dispatch_after(popTime, dispatch_get_main_queue(), ^(void){ 17 [self removeFromSuperview]; // 调主线程,从父视图移除 18 }); 19 }
SMPageControl github:https://github.com/Spaceman-Labs/SMPageControl
这个第三方控件使得UIPagecontrol 自定义使用起来更加自由方便。
1 #pragma mark private M 2 + (UIPageControl *)p_pageControl{ 3 UIImage *pageIndicatorImage = [UIImage imageNamed:@"intro_dot_unselected"]; 4 UIImage *currentPageIndicatorImage = [UIImage imageNamed:@"intro_dot_selected"]; 5 6 if (!kDevice_Is_iPhone6 && !kDevice_Is_iPhone6Plus) { 7 CGFloat desginWidth = 375.0;//iPhone6 的设计尺寸 8 CGFloat scaleFactor = kScreen_Width/desginWidth; 9 pageIndicatorImage = [pageIndicatorImage scaleByFactor:scaleFactor]; // 缩放图片 NYXImagesKit NYXImagesKit 包含一组很有用的 UIImage 图像处理方法,包括 filtering, blurring, enhancing, masking, reflecting, resizing, rotating, saving. 同时也提供了一个 UIImageView 的之类,支持异步的从 URL 下载图像并显示 https://github.com/Nyx0uf/NYXImagesKit 10 currentPageIndicatorImage = [currentPageIndicatorImage scaleByFactor:scaleFactor]; 11 } 12 13 SMPageControl *pageControl = [SMPageControl new]; // SMPageControl 自定义UIPageControl的外观,包括形状、大小、间距等,也可以用图片代替UIPageControl上的小圆点。 https://github.com/Spaceman-Labs/SMPageControl 14 pageControl.pageIndicatorImage = pageIndicatorImage; 15 pageControl.currentPageIndicatorImage = currentPageIndicatorImage; 16 [pageControl sizeToFit]; 17 return (UIPageControl *)pageControl; 18 }
其中在对SMPageControl pageIndicatorImage 处理的时候调用了一个功能强大图片处理库NYXImagesKit , 下篇会做详细讲解。