一个漂亮的 iOS 步行数小插件,支持iOS 10,支持iMessage扩展

分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友 微信微信
查看查看405 回复回复4 收藏收藏 分享淘帖 转播转播 分享分享 微信
查看: 405|回复: 4
收起左侧

一个漂亮的 iOS 步行数小插件,支持iOS 10,支持iMessage扩展

[复制链接]
swifter 发表于 2016-9-29 19:46:27 | 显示全部楼层 |阅读模式
快来登录
获取优质的苹果资讯内容
收藏热门的iOS等技术干货
拷贝下载Swift Demo源代码
订阅梳理好了的知识点专辑

iOS-10-Steps-Widget

A widget app showing step count and moved distance with beautiful chart for iOS 10, iMessage extension included.

一个漂亮的 iOS 步行数小插件 1

一个漂亮的 iOS 步行数小插件,支持iOS 10,支持iMessage扩展 - 敏捷大拇指 - 一个漂亮的 iOS 步行数小插件 1


一个漂亮的 iOS 步行数小插件 2

一个漂亮的 iOS 步行数小插件,支持iOS 10,支持iMessage扩展 - 敏捷大拇指 - 一个漂亮的 iOS 步行数小插件 2


一个漂亮的 iOS 步行数小插件 3

一个漂亮的 iOS 步行数小插件,支持iOS 10,支持iMessage扩展 - 敏捷大拇指 - 一个漂亮的 iOS 步行数小插件 3


github

A detailed blog here.

转发给大拇指各位,如下:




首先声明这并不是一篇实战教程,只是记录下开发中遇到的零零碎碎的问题和自己的解决办法。

iOS 10 最不能让我接受的改版就是 Health.app,应用层级变多,现在要点好几下才能看到自己每周的步数和运动距离统计。与此同时 iOS 10 的 Widgets 面板给了 Widget 更多的空间,想着能有一款让人一目了然地查看一周运动步数的 Widget 就好了,然而找了下 App Store 上目前并没有很好的成品,于是自己动手开写,途中顺便也尝试了下 iMessage Extension。

先扔 Repo 地址:https://github.com/Wildog/iOS-10-Steps-Widget

效果图:

一个漂亮的 iOS 步行数小插件,支持iOS 10,支持iMessage扩展 1

一个漂亮的 iOS 步行数小插件,支持iOS 10,支持iMessage扩展 - 敏捷大拇指 - 一个漂亮的 iOS 步行数小插件,支持iOS 10,支持iMessage扩展 1





1、iOS 10 Widget 新特性:折叠/展开

如上面的效果图所示,iOS 10 通过展开的方式给了 Widget 更多空间,展开状态NCWidgetDisplayModeExpanded下的最大高度是系统决定的,折叠状态 NCWidgetDisplayModeCompact下的高度则是固定的(110 左右),在 TodayViewController 中可以通过 NCWidgetProviding 协议中的 widgetActiveDisplayModeDidChange: 获取状态变化并设置需要的高度:

[Objective-C] 纯文本查看 复制代码
- (void)widgetActiveDisplayModeDidChange:(NCWidgetDisplayMode)activeDisplayMode withMaximumSize:(CGSize)maxSize {
    if (activeDisplayMode == NCWidgetDisplayModeExpanded) {
        self.preferredContentSize = CGSizeMake(0.0, 280.0);
    } else if (activeDisplayMode == NCWidgetDisplayModeCompact) {
        self.preferredContentSize = maxSize;
    }
}





2、获取 HealthKit 信息



2.1、新的访问控制

iOS 10 中增加了新的隐私访问控制,需要在 info.plist 中设置 NSHealthShareUsageDescription 和 NSHealthUpdateUsageDescription 的值作为数据使用的描述,两个值都必须设置,否则会收到类似下面的通知:

This app has crashed because it attempted to access privacy-sensitive data without a usage description. The app's Info.plist must contain an NSHealthShareUsageDescription key with a string value explaining to the user how the app uses this data.


如下图设置好这两个值过后就能正常地请求应用授权了:

一个漂亮的 iOS 步行数小插件,支持iOS 10,支持iMessage扩展 2

一个漂亮的 iOS 步行数小插件,支持iOS 10,支持iMessage扩展 - 敏捷大拇指 - 一个漂亮的 iOS 步行数小插件,支持iOS 10,支持iMessage扩展 2




2.2、异步请求带来的问题

获取 HealthKit 数据的方式是向 HKHealthStore 的实例发送 executeQuery: 信息,而这个方法是异步调用的。我一开始的做法是在 viewDidLoad 中直接调用一个 queryHealthData 方法,在这个方法里面执行一系列(一周的数据,按天请求)的 executeQuery: ,返回后再交给图表绘制。然而数据交给图表时几乎不可能是完整的,因为 executeQuery: 的异步请求此时并没有执行完,最终导致应用崩溃。所以需要一个办法在所有的异步请求全部处理完之后再进行其它处理,dispatch_group 可以很好的解决,同时 dispatch_group 内部的任务也是并发进行的:

[Objective-C] 纯文本查看 复制代码
// 创建 dispatch_group
dispatch_group_t hkGroup = dispatch_group_create();
// 依次执行请求
for (......) {
    // 创建 query
    HKStatisticsQuery *query = [[HKStatisticsQuery alloc]
                        initWithQuantityType:stepType
                        quantitySamplePredicate:predicate
                        options:HKStatisticsOptionCumulativeSum
                        completionHandler:^(HKStatisticsQuery *query, HKStatistics *result, NSError *error) {
        double data = [result.sumQuantity doubleValueForUnit:[HKUnit countUnit]];
        [arrayForData addObject:[NSNumber numberWithDouble:data]];
        // 数据存储完后离开 dispatch_group,可以理解为信号量 +1
        dispatch_group_leave(hkGroup);
    }];
    // 执行异步请求前进入 dispatch_group,可以理解为信号量 -1
    dispatch_group_enter(hkGroup);
    [self.healthStore executeQuery:query];
}
// 最后等待所有异步请求完成
dispatch_group_notify(hkGroup, dispatch_get_main_queue(),^{
    // 通知主线程绘制图表
}




2.3、锁屏状态下无法访问 HealthKit 数据

如果你尝试在锁屏状态下通过 Widget 访问 HealthKit 数据,你会在 Console 中收到类似下面的信息:

[Plain Text] 纯文本查看 复制代码
Widget[3459:674785] Error Domain=com.apple.healthkit Code=6 "Protected health data is inaccessible" UserInfo={NSLocalizedDescription=Protected health data is inaccessible}


很遗憾在锁屏状态下由于隐私保护是没法访问 HealthKit 信息的,所以我们需要对此类错误进行处理并缓存之前的数据用于锁屏状态下显示。由于数据很简单而且数量不多,数据的缓存用 NSUserDefaults 实现相当简单:

[Objective-C] 纯文本查看 复制代码
NSUserDefaults userDefaults = [[NSUserDefaults alloc] init];
// 保存数据
[userDefaults setObject:arrayForData forKey:@"snapshot"];
// 恢复数据
NSArray *arrayForData = [userDefaults arrayForKey:@"snapshot"];


然后我们在之前 query 的 completionHandler 块中加入对错误的检测并在图表上显示对应提示,并设置一个全局的 flag 用于检测到错误时显示之前缓存的数据。




3、绘制线形图

一个漂亮的 iOS 步行数小插件,支持iOS 10,支持iMessage扩展 3

一个漂亮的 iOS 步行数小插件,支持iOS 10,支持iMessage扩展 - 敏捷大拇指 - 一个漂亮的 iOS 步行数小插件,支持iOS 10,支持iMessage扩展 3




3.1、绘制渐变

通过得到的数据绘制好一条 BezierPath 路径后,再利用这个路径创建一个 CAShapeLayer 形状层,设置好属性和动画后,再创建一个和这个 View 一样大的 CAGradientLayer 渐变层,最后将渐变层作为 sublayer 添加到 self.layer 上,并把渐变层的 mask 属性设置为之前创建的 CAShapeLayer 形状层就实现了:

[Objective-C] 纯文本查看 复制代码
// 创建形状层
CAShapeLayer *chartLineShape = [CAShapeLayer layer];
chartLineShape.path          = chartLine.CGPath;
chartLineShape.lineWidth     = self.chartLineWidth;
chartLineShape.strokeColor   = [UIColor colorWithHue:0.52 saturation:1 brightness:0.83 alpha:1].CGColor;
chartLineShape.fillColor     = [UIColor clearColor].CGColor;
chartLineShape.lineCap       = kCALineCapRound;
chartLineShape.lineJoin      = kCALineJoinRound;

// 创建动画
CABasicAnimation *drawAnimation   = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
drawAnimation.duration            = self.animationDuration;
drawAnimation.repeatCount         = 1.0;
drawAnimation.removedOnCompletion = YES;
drawAnimation.fromValue           = [NSNumber numberWithFloat:0.0f];
drawAnimation.toValue             = [NSNumber numberWithFloat:1.0f];
drawAnimation.timingFunction      = [CAMediaTimingFunction functionWithControlPoints: 0.348 : 0.000 : 0.285 : 0.743];
[chartLineShape addAnimation:drawAnimation forKey:@"drawChartLineAnimation"];

// 创建渐变层
CAGradientLayer *gradientLayer = [CAGradientLayer layer];
gradientLayer.frame            = CGRectMake(0, 0, self.frame.size.width, self.frame.size.height);
gradientLayer.colors           = @[(__bridge id)[UIColor colorWithHue:0.57 saturation:0.74 brightness:0.86 alpha:1].CGColor,(__bridge id)[UIColor colorWithHue:0.52 saturation:1 brightness:0.76 alpha:1].CGColor, (__bridge id)[UIColor colorWithHue:0.52 saturation:1 brightness:0.83 alpha:1].CGColor];
gradientLayer.startPoint       = CGPointMake(0,0.5);
gradientLayer.endPoint         = CGPointMake(1,0.5);

// 设置遮罩
[self.layer addSublayer:gradientLayer];
gradientLayer.mask = chartLineShape;




3.2、检测触摸点击

我单独写了一个 ChartNodeView 来表示和绘制节点,并在 ChartView 的 drawRect: 中创建节点并将它们作为 subview 添加进来,触摸点击节点会触发动画效果和显示节点的相关信息,所以需要检测触摸事件并通过 hitTest: 识别触摸对象来判断节点序号:

[Objective-C] 纯文本查看 复制代码
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    [self touchPoint:touches withEvent:event];
    [super touchesBegan:touches withEvent:event];
}

- (void)touchPoint:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch *touch = [touches anyObject];
    CGPoint touchPoint = [touch locationInView:self];
    UIView *touchView = [self hitTest:touchPoint withEvent:nil];
    
    // 判断触摸对象是否为节点
    if ([touchView isKindOfClass:[ChartNodeView class]]) {
        // 重新高亮节点
        for (UIView *subview in self.subviews) {
            if ([subview isKindOfClass:[ChartNodeView class]]) {
                ChartNodeView *nodeView = (ChartNodeView*)subview;
                if (nodeView.isActive) [nodeView toggleState];
            }
        }
        ChartNodeView *touchNode = (ChartNodeView*)touchView;
        [touchNode toggleState];
        // 显示节点信息的逻辑交由 delegate 处理
        _lastSelected = touchNode.index;
        if ([self.delegate respondsToSelector:@selector(clickedNodeAtIndex:)]) {
            [self.delegate clickedNodeAtIndex:_lastSelected];
        }
    }
}


这个线型图的实现很简单,写的时候也注意了一定的可复用性,如果有需要的话可以直接拿走用。




4、iOS 10 iMessage Extension

一个漂亮的 iOS 步行数小插件,支持iOS 10,支持iMessage扩展 4

一个漂亮的 iOS 步行数小插件,支持iOS 10,支持iMessage扩展 - 敏捷大拇指 - 一个漂亮的 iOS 步行数小插件,支持iOS 10,支持iMessage扩展 4


iOS 10 提供的 iMessage 扩展可以生成漂亮的 rich message。把之前 Widget 里写好的 ViewController 拿过来改改就可以直接用,这里记录下信息的生成,使用 MSMessageTemplateLayout 可以创建带媒体文件、标题和说明的布局,创建 NSMessage 后,设置其布局属性,然后通过 MSMessagesAppViewController 的 activeConversation 属性获取当前对话并执行 insertMessage: 来插入信息,整个过程完毕后交给用户添加评论或发送:

[Objective-C] 纯文本查看 复制代码
MSMessageTemplateLayout *layout = [[MSMessageTemplateLayout alloc] init];
layout.image = image;
layout.caption = @"This is a caption";

MSMessage *msg = [[MSMessage alloc] init];
msg.layout = layout;
msg.URL = [NSURL URLWithString:@"emptyURL"];

[self.activeConversation insertMessage:msg completionHandler:^(NSError *error){
    // error handling
}];


为了保证得到的是操作之后的实时结果,这里得到 image 的方式是直接获取 presentation layer 的 graphics context 并创建位图,注意使用的方法是 UIGraphicsBeginImageContextWithOptions(CGSize size, BOOL opaque, CGFloat scale) 且 scale 的值需要设为 0,表示 scale factor 由设备决定,如果使用 UIGraphicsBeginImageContext(CGSize size) 的话默认的 scale 值为 1,在 2x, 3x 设备上会显示模糊的图像:

[Objective-C] 纯文本查看 复制代码
UIGraphicsBeginImageContextWithOptions(CGSizeMake(self.lineChartView.frame.size.width, self.lineChartView.frame.size.height), NO, 0);
[self.lineChartView drawViewHierarchyInRect:CGRectMake(0, 0, self.lineChartView.frame.size.width, self.lineChartView.frame.size.height) afterScreenUpdates:YES];
CGContextRef context = UIGraphicsGetCurrentContext();
[self.view.layer.presentationLayer renderInContext:context];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();





5、顺便吐槽 Xcode 8

升级之后整个人都懵逼了,所有的插件都没了,包括没法离开的 XVim,看了下 Alcatraz Issues 里面的讨论,官方采用了 runtime library validation 对 Xcode 插件进行验证,并提供了新的 Xcode 插件协议 Xcode Source Editor Extensions(然而目前只能提供基本的替换功能),主要是为了防止 XcodeGhost 这样的恶意插件。废话说得再多,我依然不能接受。翻遍 Github 找到了个很好用的工具 inket/update_xcode_plugins,不光可以 unsign Xcode 使其重新支持插件,还可以给插件自动添加 UUID,经过十分钟的拷贝和 unsign,Xcode 8 总算是能正常使用了。

都看到这里了,就把这篇资料推荐给您的好朋友吧,让他们也感受一下。

回帖是一种美德,也是对楼主发帖的尊重和支持。

*声明:敏捷大拇指是全球最大的Swift开发者社区、苹果粉丝家园、智能移动门户,所载内容仅限于传递更多最新信息,并不意味赞同其观点或证实其描述;内容仅供参考,并非绝对正确的建议。本站不对上述信息的真实性、合法性、完整性做出保证;转载请注明来源并加上本站链接,敏捷大拇指将保留所有法律权益。如有疑问或建议,邮件至marketing@swifthumb.com

*联系:微信公众平台:“swifthumb” / 腾讯微博:@swifthumb / 新浪微博:@swifthumb / 官方QQ一群:343549891(满) / 官方QQ二群:245285613 ,需要报上用户名才会被同意进群,请先注册敏捷大拇指

嗯,不错!期待更多好内容,支持一把:
支持敏捷大拇指,用支付宝支付10.24元 支持敏捷大拇指,用微信支付10.24元

评分

参与人数 1金钱 +10 贡献 +10 专家分 +10 收起 理由
Anewczs + 10 + 10 + 10 32个赞!专家给力!

查看全部评分

本帖被以下淘专辑推荐:

rightnow 发表于 2016-10-1 00:08:40 | 显示全部楼层
问题是,我们的关系都不只在iMessage上啊
rightnow 发表于 2016-10-1 00:09:47 | 显示全部楼层
防止 XcodeGhost 这样的恶意插件


这倒是应该的!
linus 发表于 2016-10-4 18:56:08 | 显示全部楼层
是不是所有App都可以做个iMessage扩展?
买定离手 发表于 前天 11:46 | 显示全部楼层
苹果应该搞个Xcode 插件库!
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

做任务,领红包。
我要发帖

分享扩散

都看到这里了,就把这资料推荐给您的好朋友吧,让他们也感受一下。
您的每一位朋友访问此永久链接后,您都将获得相应的金钱积分奖励
关闭

站长推荐 上一条 /3 下一条

热门推荐

合作伙伴

Swift小苹果

  • 北京治世天下科技有限公司
  • ©2014-2016 敏捷大拇指
  • 京ICP备14029482号
  • Powered by Discuz! X3.1 Licensed
  • swifthumb Wechat Code
  •   
快速回复 返回顶部 返回列表