應用很早就上線了,歡迎大家下載使用: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);
}
}
額外的討論: