应用很早就上线了,欢迎大家下载使用:http://itunes.apple.com/app/id1206687109
源码已经公开,大家可以去https://github.com/Inspirelife96/ILDiligence下载。 喜欢的话Fork或者给个Star,非常感谢。
下面是这一系列的全部帖子:
想法和原型
勤之时 - 架构与工程组织结构
勤之时 - 数据持久层的实现
勤之时 - 网络层的实现
勤之时 - 业务逻辑层
勤之时 - Info.plist的改动
勤之时 - 表示层(一)
勤之时 - 表示层(二)
勤之时 - 表示层(三)
勤之时 - 表示层(四)
勤之时 - 表示层(五)
表示层:由UIKit Framework构成,也就是我们看到的视图,控制器,各种控件以及事件处理等内容。
首先来谈谈表示层的架构,继续推荐大神的iOS应用架构谈 view层的组织和调用方案
说下【勤之时】最后适用的要点:
以下内容摘抄自iOS应用架构谈 view层的组织和调用方案
- 所有的属性都使用getter和setter
不要在viewDidLoad里面初始化你的view然后再add,这样代码就很难看。在viewDidload里面只做addSubview的事情,然后在viewWillLayoutSubviews里面做布局的事情,最后在viewDidAppear里面做Notification的监听之类的事情。至于属性的初始化,则交给getter去做。 例如:
#pragma mark - Life Cycle
- (void)viewDidLoad {
[super viewDidLoad];
[self.view addSubview:self.taskScrollView];
[self.view addSubview:self.pageControl];
}
- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
[self.taskScrollView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self.view);
[self.taskScrollView setContentSize:CGSizeMake(CGRectGetWidth(_taskScrollView.frame) * self.taskIds.count, CGRectGetHeight(_taskScrollView.frame))];
}];
[self.pageControl mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.view.mas_top).with.offset(30);
make.centerX.equalTo(self.view);
make.height.mas_equalTo(28);
make.width.mas_equalTo(ScreenWidth - 42 * 2);
}];
}
#pragma mark - Getter and Setter
- (UIScrollView *)taskScrollView {
if (!_taskScrollView) {
_taskScrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, MainScreenWidth, MainScreenHeight)];
NSMutableArray *controllers = [[NSMutableArray alloc] init];
for (NSUInteger i = 0; i < self.taskIds.count; i++)
{
[controllers addObject:[NSNull null]];
}
self.viewControllers = controllers;
_taskScrollView.pagingEnabled = YES;
_taskScrollView.contentSize = CGSizeMake(CGRectGetWidth(_taskScrollView.frame) * self.taskIds.count, CGRectGetHeight(_taskScrollView.frame));
_taskScrollView.showsHorizontalScrollIndicator = NO;
_taskScrollView.showsVerticalScrollIndicator = NO;
_taskScrollView.scrollsToTop = NO;
_taskScrollView.delegate = self;
}
return _taskScrollView;
}
- (UIPageControl *)pageControl {
if (!_pageControl) {
_pageControl = [[UIPageControl alloc] init];
self.pageControl.numberOfPages = self.taskIds.count;
self.pageControl.currentPage = 0;
}
return _pageControl;
}
- getter和setter全部都放在最后
因为一个ViewController很有可能会有非常多的view,就像上面给出的代码样例一样,如果getter和setter写在前面,就会把主要逻辑扯到后面去,其他人看的时候就要先划过一长串getter和setter,这样不太好。然后要求业务工程师写代码的时候按照顺序来分配代码块的位置,先是life cycle,然后是Delegate方法实现,然后是event response,然后才是getters and setters。这样后来者阅读代码时就能省力很多。
- 每一个delegate都把对应的protocol名字带上,delegate方法不要到处乱写,写到一块区域里面去
比如UITableViewDelegate的方法集就老老实实写上#pragma mark - UITableViewDelegate。这样有个好处就是,当其他人阅读一个他并不熟悉的Delegate实现方法时,他只要按住command然后去点这个protocol名字,Xcode就能够立刻跳转到对应这个Delegate的protocol定义的那部分代码去,就省得他到处找了。
- event response专门开一个代码区域
所有button、gestureRecognizer的响应事件都放在这个区域里面,不要到处乱放。
- 关于private methods,正常情况下ViewController里面不应该写
不是delegate方法的,不是event response方法的,不是life cycle方法的,就是private method了。对的,正常情况下ViewController里面一般是不会存在private methods的,这个private methods一般是用于日期换算、图片裁剪啥的这种小功能。这种小功能要么把它写成一个category,要么把他做成一个模块,哪怕这个模块只有一个函数也行。
ViewController基本上是大部分业务的载体,本身代码已经相当复杂,所以跟业务关联不大的东西能不放在ViewController里面就不要放。另外一点,这个private method的功能这时候只是你用得到,但是将来说不定别的地方也会用到,一开始就独立出来,有利于将来的代码复用。
关于View的布局
- 【勤之时】使用了Masonry
何时使用storyboard,何时使用nib,何时使用代码写View
- 【勤之时】使用代码
关于MVC、MVVM等一大堆思想
- 【勤之时】使用MVC
我会分好几个篇来说明表示层的开发。虽然这个应用的内容不多,但还是有几个页面的。首先来看看主界面:【勤之时】用来计时学习的界面:
功能描述:
- 背景为每日一图
- 主界面四个角落有四个按钮,点击可以进入各自的功能页面,分别为:
- 任务管理
- 统计
- 每日分享
- 设置
- 顶部有一个Page Controller,有多少个小圆点就代表有多少个任务。通过左右滑动,可以切换到不同的任务。
- 中上部为当前任务名称及当前的日历。同样,当左右滑动时,切换为不同的任务。
- 背景图片在左右滑动是不会移动,每个任务有不同的配色,根据配色会对背景图增加一层对应的遮罩。
- 中下部为【勤·开始】按钮,点击开始任务计时。
- 计时时,会显示暂停按钮(若为沉浸模式,则无暂停按钮。)背景音乐开始播放(若背景音乐关闭,则不播放)。中上部圆盘内显示倒计时,圆环同时开始进度显示。
- 点击暂停按钮,则出现继续/放弃按钮。音乐停,倒计时停。
- 计时完成时,提示计时完成(蜂鸣+手机震动)。主页面切换为休息建议,并出现【勤·休息】按钮以及跳过按钮。
- 若按【勤·休息】按钮,进入休息倒计时。
- 若按跳过按钮,则直接回到默认的【勤·开始】页面。
- 在倒计时过程中,所有其他额外的按钮都会隐藏。
- 应用最小化后,音乐和倒计时会继续。
- 在沉浸模式,倒计时时没有暂停按钮。最小化应用会直接退出倒计时,此时倒计时和应用都会暂停,回到默认的【勤·开始】页面
MVC设计考虑:
Controller:
ILDDiligenceViewController:
page controller结合scrollview,以page的模式来显示ILDDiligenceClockViewController的View的内容。 当然,其他所有的按钮都在这个Controller中定义,包括四角的四个功能按钮,以及开始,暂停,继续,放弃,休息等按钮。ILDDiligenceClockViewController:每一个Page的ViewController,主要包括背景颜色的遮罩,圆环以及圆环内的任务名称,日期,倒计时显示,休息建议等。每一个Page代表一个任务。
Model:
ILDDiligenceViewController对应的Model
NSArray:所有任务的Ids
ILDTaskModel:当前任务的具体内容
ILDStoryModel: 背景图片的内容
ILDDiligenceClockViewController对应的Model
ILDTaskModel:当前任务的具体内容
View:
- ILDDiligenceClockViewController对应的View
- ILDDiligenceClockView:用于具体描绘每个ClockView的类
ILDDiligenceViewController编码
- 四个角的四个功能按钮,定义
@interface ILDDiligenceViewController ()
@property(nonatomic, strong) UIButton *taskButton;
@property(nonatomic, strong) UIButton *statisticsButton;
@property(nonatomic, strong) UIButton *storyButton;
@property(nonatomic, strong) UIButton *settingButton;
@end
在viewDidLoad中将这些按钮添加到Controller的View中
- (void)viewDidLoad {
[super viewDidLoad];
[self.view addSubview:self.taskButton];
[self.view addSubview:self.statisticsButton];
[self.view addSubview:self.storyButton];
[self.view addSubview:self.settingButton];
}
在viewWillLayoutSubviews中设定Layout
- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
[self.taskButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.view.mas_top).with.offset(30);
make.left.equalTo(self.view.mas_left).with.offset(12);
make.height.mas_equalTo(28);
make.width.mas_equalTo(28);
}];
[self.statisticsButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.view.mas_top).with.offset(30);
make.right.equalTo(self.view.mas_right).with.offset(-12);
make.height.mas_equalTo(28);
make.width.mas_equalTo(28);
}];
[self.storyButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.view.mas_bottom).with.offset(-50);
make.left.equalTo(self.view.mas_left).with.offset(12);
make.height.mas_equalTo(28);
make.width.mas_equalTo(28);
}];
[self.settingButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.view.mas_bottom).with.offset(-50);
make.right.equalTo(self.view.mas_right).with.offset(-12);
make.height.mas_equalTo(28);
make.width.mas_equalTo(28);
}];
}
在get函数中初始化
- (UIButton *)taskButton {
if (!_taskButton) {
_taskButton = [UIButton buttonWithType:UIButtonTypeCustom];
[_taskButton setImage:[UIImage imageNamed:@"menu_task_28x28_"] forState:UIControlStateNormal];
[_taskButton addTarget:self action:@selector(clickTaskButton:) forControlEvents:UIControlEventTouchUpInside];
}
return _taskButton;
}
- (UIButton *)statisticsButton {
if (!_statisticsButton) {
_statisticsButton = [UIButton buttonWithType:UIButtonTypeCustom];
[_statisticsButton setBackgroundImage:[UIImage imageNamed:@"menu_statistics_28x28_"] forState:UIControlStateNormal];
[_statisticsButton addTarget:self action:@selector(clickStatisticsButton:) forControlEvents:UIControlEventTouchUpInside];
}
return _statisticsButton;
}
- (UIButton *)storyButton {
if (!_storyButton) {
_storyButton = [[UIButton alloc] init];
[_storyButton setBackgroundImage:[UIImage imageNamed:@"menu_story_28x28_"] forState:UIControlStateNormal];
[_storyButton addTarget:self action:@selector(clickStoryButton:) forControlEvents:UIControlEventTouchUpInside];
}
return _storyButton;
}
- (UIButton *)settingButton {
if (!_settingButton) {
_settingButton = [[UIButton alloc] init];
[_settingButton setBackgroundImage:[UIImage imageNamed:@"menu_settings_26x26_"] forState:UIControlStateNormal];
[_settingButton addTarget:self action:@selector(clickSettingButton:) forControlEvents:UIControlEventTouchUpInside];
}
return _settingButton;
}
按钮对应的Event函数
- (void)clickTaskButton:(id)sender {
[self copyScreen];
ILDTaskListViewController *taskListVC = [[ILDTaskListViewController alloc] init];
UINavigationController *taskListNC = [[UINavigationController alloc] initWithRootViewController:taskListVC];
[self presentViewController:taskListNC animated:YES completion:nil];
}
- (void)clickStatisticsButton:(id)sender {
[self copyScreen];
ILDStatisticsTodayViewController *statisticsTodayVC = [[ILDStatisticsTodayViewController alloc] init];
UINavigationController *settingNC = [[UINavigationController alloc] initWithRootViewController:statisticsTodayVC];
[self presentViewController:settingNC animated:YES completion:nil];
}
- (void)clickStoryButton:(id)sender {
ILDStoryViewController *storyVC = [[ILDStoryViewController alloc] init];
[self presentViewController:storyVC animated:YES completion:nil];
}
- (void)clickSettingButton:(id)sender {
[self copyScreen];
ILDSettingViewController *settingVC = [[ILDSettingViewController alloc] init];
UINavigationController *settingNC = [[UINavigationController alloc] initWithRootViewController:settingVC];
[self presentViewController:settingNC animated:YES completion:nil];
}
其他控件的操作基本上同上,以同样的方式处理ScrollView和PageView,然后再添加对应的ScrollViewDeligate
- (void)loadScrollViewWithPage:(NSUInteger)page {
if (page >= self.taskIds.count) {
return;
}
// replace the placeholder if necessary
ILDDiligenceClockViewController *controller = [self.viewControllers objectAtIndex:page];
if ((NSNull *)controller == [NSNull null]) {
controller = [[ILDDiligenceClockViewController alloc] init];
controller.taskId = self.taskIds[page];
controller.diligenceClockView.delegate = self;
controller.isRestMode = NO;
[self.viewControllers replaceObjectAtIndex:page withObject:controller];
}
// add the controller's view to the scroll view
if (controller.view.superview == nil) {
CGRect frame = self.clockScrollView.frame;
frame.origin.x = CGRectGetWidth(frame) * page;
frame.origin.y = 0;
controller.view.frame = frame;
[self addChildViewController:controller];
[self.clockScrollView addSubview:controller.view];
[controller didMoveToParentViewController:self];
}
}
// at the end of scroll animation, reset the boolean used when scrolls originate from the UIPageControl
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
// switch the indicator when more than 50% of the previous/next page is visible
CGFloat pageWidth = CGRectGetWidth(self.clockScrollView.frame);
NSUInteger page = floor((self.clockScrollView.contentOffset.x - pageWidth / 2) / pageWidth) + 1;
self.pageControl.currentPage = page;
// load the visible page and the page on either side of it (to avoid flashes when the user starts scrolling)
[self loadScrollViewWithPage:page - 1];
[self loadScrollViewWithPage:page];
[self loadScrollViewWithPage:page + 1];
// a possible optimization would be to unload the views+controllers which are no longer visible
}
- (void)gotoPage:(BOOL)animated {
NSInteger page = self.pageControl.currentPage;
// load the visible page and the page on either side of it (to avoid flashes when the user starts scrolling)
[self loadScrollViewWithPage:page - 1];
[self loadScrollViewWithPage:page];
[self loadScrollViewWithPage:page + 1];
// update the scroll view to the appropriate page
CGRect bounds = self.clockScrollView.bounds;
bounds.origin.x = CGRectGetWidth(bounds) * page;
bounds.origin.y = 0;
[self.clockScrollView scrollRectToVisible:bounds animated:animated];
}
点击开始按钮是,我们需要播放背景音乐,所以需要引入AVAudioPlayer
@property(nonatomic, strong) AVAudioPlayer *musicPlayer;
- (void)playMusic {
self.musicPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:[MusicHelper musicUrlByName:self.currentTaskModel.musicName] error:nil];
self.musicPlayer.delegate = self;
self.musicPlayer.numberOfLoops = -1;
self.musicPlayer.volume = 1;
[self.musicPlayer prepareToPlay];
self.musicPlayer.meteringEnabled = YES;
[self.musicPlayer play];
}
- (void)pauseMusic {
[self.musicPlayer pause];
}
- (void)stopMusic {
[self.musicPlayer stop];
}
- (void)resumeMusic {
[self.musicPlayer play];
}
每次计时完成时,需要有提示声及振动:
- (void)playSystemSound {
SystemSoundID sound = kSystemSoundID_Vibrate;
//这里使用在上面那个网址找到的铃声,注意格式
NSString *path = [NSString stringWithFormat:@"/System/Library/Audio/UISounds/%@.%@",@"new-mail",@"caf"];
if (path) {
OSStatus error = AudioServicesCreateSystemSoundID((__bridge CFURLRef)[NSURL fileURLWithPath:path],&sound);
if (error != kAudioServicesNoError) {
sound = 0;
}
}
AudioServicesPlaySystemSound(sound);//播放声音
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);//静音模式下震动
}
每次任务完成后,需要把任务的统计信息保存起来
- (void)taskCompleted {
NSInteger page = self.pageControl.currentPage;
ILDDiligenceClockViewController *controller = [self.viewControllers objectAtIndex:page];
[self stopMusic];
[self playSystemSound];
if (controller.isRestMode) {
[self setToDiligenceStartStatus];
} else {
ILDDiligenceModel *diligencModel = [[ILDDiligenceModel alloc] init];
diligencModel.taskId = self.taskIds[page];
diligencModel.startDate = self.startDate;
diligencModel.endDate = [NSDate date];
diligencModel.breakTimes = [NSNumber numberWithInteger:self.breakTimes];
diligencModel.diligenceTime = self.currentTaskModel.diligenceTime;
[[ILDDiligenceDataCenter sharedInstance] addDiligence:diligencModel];
if (self.currentTaskModel.isRestModeEnabled) {
[self setToRestStartStatus];
} else {
[self setToDiligenceStartStatus];
}
}
}
应用最小化时,要根据是否是沉浸模式,继续计时和音乐或关闭计时和音乐,此时需要监听UIApplicationDidEnterBackgroundNotification,使用NSNotificationCenter
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillEnterBackground:)name:UIApplicationDidEnterBackgroundNotification object:nil];
需要监听ILDStoryDataCenter的storyDataDictionary成员,当变化时,背景图片也要相应的变化,使用KVO
[[ILDStoryDataCenter sharedInstance] addObserver:self forKeyPath:@"storyDataDictionary" options:NSKeyValueObservingOptionNew context:NULL];
ILDDiligenceViewController编码
这个类相对简单
首先根据当前的任务设定背景颜色
self.taskModel = [[ILDTaskDataCenter sharedInstance] taskConfigurationById:self.taskId];
self.backgroundView.backgroundColor = [ColorHelper colorByName:self.taskModel.color];
其他的事情由ILDDiligenceClockView来处理,他仅需要把ILDDiligenceClockView添加到自己的view中。
ILDDiligenceClockView编码
主要代码就是画时钟及其内容
- (void)drawRect:(CGRect)rect {
// calculate angle for progress
if (self.diligenceSeconds == 0) {
self.endAngle = self.startAngle;
} else {
self.endAngle = (1 - self.timeLeft / self.diligenceSeconds) * 2 * M_PI + self.startAngle;
}
CGFloat radius = (rect.size.width - 20)/2;
// draw circle
UIBezierPath *circle = [UIBezierPath bezierPath];
[circle addArcWithCenter:CGPointMake(rect.size.width / 2, rect.size.height / 2)
radius:radius
startAngle:0
endAngle:2 * M_PI
clockwise:YES];
circle.lineWidth = CIRCLE_WIDTH;
[FlatWhiteDark setStroke];
[circle stroke];
// draw progress
UIBezierPath *progress = [UIBezierPath bezierPath];
[progress addArcWithCenter:CGPointMake(rect.size.width / 2, rect.size.height / 2)
radius:radius
startAngle:self.startAngle
endAngle:self.endAngle
clockwise:YES];
progress.lineWidth = PROGRESS_WIDTH;
[FlatWhite setStroke];
[progress stroke];
if (self.isRunning) {
// if Timer is running, always show time left in the center of the circle
NSString *textContent = [ILDDateHelper minutesFormatBySeconds:self.timeLeft];
UIFont *textFont = [UIFont fontWithName: @"-" size: TEXT_NAME_SIZE];
CGSize textSize = [textContent sizeWithAttributes:@{NSFontAttributeName:textFont}];
CGRect textRect = CGRectMake(rect.size.width / 2 - textSize.width / 2,
rect.size.height / 2 - textSize.height / 2,
textSize.width , textSize.height);
NSMutableParagraphStyle *textStyle = [[NSMutableParagraphStyle defaultParagraphStyle] mutableCopy];
textStyle.lineBreakMode = NSLineBreakByWordWrapping;
textStyle.alignment = NSTextAlignmentCenter;
[textContent drawInRect:textRect withAttributes:@{NSFontAttributeName:textFont, NSForegroundColorAttributeName:FlatWhite, NSParagraphStyleAttributeName:textStyle}];
} else {
// show task Name or rest suggestion Name
NSString *taskOrRestName = self.taskName;
if (self.isRestMode) {
taskOrRestName = [ILDRestSuggestion randomRestSuggestion];
}
NSInteger fontSize = TEXT_NAME_SIZE;
UIFont *taskNameFont = [UIFont fontWithName: @"-" size: fontSize];
CGSize taskNameSize = [taskOrRestName sizeWithAttributes:@{NSFontAttributeName:taskNameFont}];
while (taskNameSize.width > (self.frame.size.width - 20)) {
fontSize -= 2;
taskNameFont = [UIFont fontWithName: @"-" size: fontSize];
taskNameSize = [taskOrRestName sizeWithAttributes:@{NSFontAttributeName:taskNameFont}];
}
CGFloat taskNameX = 10;
CGFloat taskNameY = (rect.size.height - taskNameSize.height)/2 - 10;
CGFloat taskNameWidth = self.frame.size.width - 20;
CGFloat taskNameHeight = taskNameSize.height;
CGRect taskNameRect = CGRectMake(taskNameX, taskNameY, taskNameWidth, taskNameHeight);
NSMutableParagraphStyle *textStyle = [[NSMutableParagraphStyle defaultParagraphStyle] mutableCopy];
textStyle.lineBreakMode = NSLineBreakByWordWrapping;
textStyle.alignment = NSTextAlignmentCenter;
[taskOrRestName drawInRect:taskNameRect withAttributes:@{NSFontAttributeName:taskNameFont, NSForegroundColorAttributeName:FlatWhite, NSParagraphStyleAttributeName:textStyle}];
NSString *dateToday = [ILDDateHelper stringOfDayWithWeekDay:[NSDate date]];
UIFont *dateFont = [UIFont fontWithName: @"-" size: TEXT_DATE_SIZE];
CGSize dateSize = [dateToday sizeWithAttributes:@{NSFontAttributeName:dateFont}];
CGFloat dateX = (rect.size.width - dateSize.width)/2;
CGFloat dateY = taskNameY + taskNameHeight + 5;
CGFloat dateWidth = dateSize.width;
CGFloat dateHeight = dateSize.height;
CGRect dateRect = CGRectMake(dateX, dateY, dateWidth, dateHeight);
[dateToday drawInRect:dateRect withAttributes:@{NSFontAttributeName:dateFont, NSForegroundColorAttributeName:FlatWhite, NSParagraphStyleAttributeName:textStyle}];
CGContextRef context = UIGraphicsGetCurrentContext();
[FlatWhite setStroke];
CGContextMoveToPoint(context, dateX, dateY - 2);
CGContextAddLineToPoint(context, dateX + dateWidth, dateY - 2);
CGContextMoveToPoint(context, dateX, dateY + dateHeight + 2);
CGContextAddLineToPoint(context, dateX + dateWidth, dateY + dateHeight + 2);
CGContextStrokePath(context);
}
}
额外的讨论: