美文网首页
勤之时 - 表示层(一)

勤之时 - 表示层(一)

作者: 启发禅悟 | 来源:发表于2017-03-26 14:44 被阅读236次

    应用很早就上线了,欢迎大家下载使用: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

    我会分好几个篇来说明表示层的开发。虽然这个应用的内容不多,但还是有几个页面的。首先来看看主界面:【勤之时】用来计时学习的界面:

    Diligence Layer 1.png

    功能描述:

    • 背景为每日一图
    • 主界面四个角落有四个按钮,点击可以进入各自的功能页面,分别为:
    • 任务管理
    • 统计
    • 每日分享
    • 设置
    • 顶部有一个Page Controller,有多少个小圆点就代表有多少个任务。通过左右滑动,可以切换到不同的任务。
    • 中上部为当前任务名称及当前的日历。同样,当左右滑动时,切换为不同的任务。
    • 背景图片在左右滑动是不会移动,每个任务有不同的配色,根据配色会对背景图增加一层对应的遮罩。
    • 中下部为【勤·开始】按钮,点击开始任务计时。
    Diligence Layer 2.png
    • 计时时,会显示暂停按钮(若为沉浸模式,则无暂停按钮。)背景音乐开始播放(若背景音乐关闭,则不播放)。中上部圆盘内显示倒计时,圆环同时开始进度显示。
    • 点击暂停按钮,则出现继续/放弃按钮。音乐停,倒计时停。
    • 计时完成时,提示计时完成(蜂鸣+手机震动)。主页面切换为休息建议,并出现【勤·休息】按钮以及跳过按钮。
    • 若按【勤·休息】按钮,进入休息倒计时。
    • 若按跳过按钮,则直接回到默认的【勤·开始】页面。
    • 在倒计时过程中,所有其他额外的按钮都会隐藏。
    • 应用最小化后,音乐和倒计时会继续。
    • 在沉浸模式,倒计时时没有暂停按钮。最小化应用会直接退出倒计时,此时倒计时和应用都会暂停,回到默认的【勤·开始】页面

    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);
        }
    }
    
    

    额外的讨论:

    相关文章

      网友评论

          本文标题:勤之时 - 表示层(一)

          本文链接:https://www.haomeiwen.com/subject/pchanttx.html