普通视图

发现新文章,点击刷新页面。
昨天以前NSHipster

CMMotionActivity

作者 Mattt
2018年9月10日 00:00

人类通过组合视觉、本体感觉和前庭系统得到的感觉信息来察觉自身的运动。其中前庭系统起到主要作用,前庭系统由感觉旋转的半规管和对水平与垂直受力敏感的耳石构成。

如今的 iPhone 都有着一整套传感器,包括相机、气压计、陀螺仪、磁强计和加速规。和人类一样,它们使用不同感觉信息的组合来确定其位置和朝向,通常和我们自身的生物力学过程非常相似。

让感觉输入有意义——不管它们是从哪里来的——是非常有挑战性的。需要考虑的信息实在太多了。(见鬼,我们这个种族花了小几百万年才搞定这件事,然而我们还是会被像电梯、飞机和过山车这些新奇的发明弄晕。)

经过几个版本的系统与硬件更新,苹果的设备变得擅长于区分不同运动方式。在你跑去尝试自己实现前,停下来考虑一下使用这周文章中讨论的内置 API。


在 iOS 和 watchOS 上,CMMotionActivityManager 处理设备中传感器的原始数据并告诉你(有多确定)用户是否正在移动,和用户是在行走、跑步、骑行或者开车。

要使用这个 API,首先创建一个活动管理器,然后使用 startActivityUpdates 方法来开始监听活动更新。每当设备更新了运动相关活动,它就会执行指定的闭包,并传入一个 CMMotionActivity 对象。

let manager = CMMotionActivityManager()
        manager.startActivityUpdates(to: .main) { (activity) in
        guard let activity = activity else {
        return
        }
        var modes: Set<String> = []
        if activity.walking {
        modes.insert("🚶‍")
        }
        if activity.running {
        modes.insert("🏃‍")
        }
        if activity.cycling {
        modes.insert("🚴‍")
        }
        if activity.automotive {
        modes.insert("🚗")
        }
        print(modes.joined(separator: ", "))
        }
        

CMMotionActivityManager 由 Core Motion 框架提供。支持 Core Motion 的设备都装备了一个运动协处理器。通过使用专用硬件,系统可以将所有传感器处理工作从 CPU 上卸载掉,并减少电量的使用。

第一款 M 系列协处理器是 M7,在 2013 年九月与 iPhone 5S 一同面世。iOS 7 和 Core Motion API 也与此同时发布。

司机功能

可能运动活动最广为人知的使用是 iOS 11 中新增的「驾驶勿扰」功能。这项功能推出后,对于开车的检测变好了很多。

在速度比较低的情况下,只使用加速规的数据是很难区分驾车行驶和其他活动的。我们只能猜测,有可能 iPhone 是使用磁强计的数据来实现这项功能的。因为汽车和其他机动车辆通常被金属围绕着,电磁通量会减少。

除了安全相关的考虑,一些应用可能会根据当前的交通模式改变行为。比如,一个快递应用可能会发送运动活动改变到服务器来重新计算预计到达时间,或者更新 UI 来表明快递员已经停下了车子,正在走路接近。

不动地移动

CMMotionActivity 对每种运动类型都有一个布尔值属性,并且还有还有一个设备是否处于静止状态的属性。这似乎违反直觉,逻辑上来说你在同一时间只能步行或者开车,但不能一起进行。

关于这一点在 CMMotionActivity 的文档中有澄清:

本类中关于运动的属性并不是互相排斥的。换句话说,有可能有多个运动相关属性的值为 true。举个例子,如果用户在驾驶一辆车,然后在红灯前停了下来,对于这个事情中运动相关的更新事件会将 cyclingstationary 属性都设为真。

文档原文:

The motion-related properties of this class are not mutually exclusive. In other words, it is possible for more than one of the motion-related properties to contain the value true. For example, if the user was driving in a car and the car stopped at a red light, the update event associated with that change in motion would have both the cycling and stationary properties set to true.

等等,我刚才说了澄清?我的意思是……反正不是和这个文档这样。(我很确定这个例子应该是 automotive)幸好,头文件的文档有比我们想知道的更多信息。下面有一些这个 API 在不同情况下具体的例子:

场景 1:你在一辆停在红灯前的车里

🚶‍ walking 🏃‍ running 🚴‍cycling 🚗 automotive 🛑 stationary
false false false true true

场景 2:你在一辆移动中的机动车里

🚶‍ walking 🏃‍ running 🚴‍cycling 🚗 automotive 🛑 stationary
false false false true false

场景 3:设备正在运动,但你没有在走路也没有在机动车辆上

🚶‍ walking 🏃‍ running 🚴‍cycling 🚗 automotive 🛑 stationary
false false false false false

场景 4:你是一位名侦探,在一辆行驶中的火车上的走廊里追逐嫌疑犯,跑到了最后一节车厢并停下来四处查看并猜测他们藏在哪。(可能在角落里那个可疑、一人大小的箱子里?)

🚶‍ walking 🏃‍ running 🚴‍cycling 🚗 automotive 🛑 stationary 🕵️‍🇧🇪 poirot
false true false true true true

(我们实际上并不确定最后一个场景会发生什么……)

总的来说这个文档的大意是,你应该认为 stationaryCMMotionActivity 中其他属性是正交的,并且你应该准备好处理所有的组合情况。

每个 CMMotionActivity 对象还包括了一个其值可能为 .low.medium.highconfidence 属性。不幸的是,文档既没有提供多少关于这些值的有用信息,也没有说明它们要如何使用。对于这种情况,推荐使用一种经验性的方法:实际测试你的应用,观察不同情况下出现的 confidence 值,使用这些信息来修正应用的行为。

与位置查询组合使用

根据你的实际情况,组合使用 Core Motion 和 Core Location 数据可能是有意义的。

你可以组合一段时间的位置变化和把握比较低的运动活动数据来提高精确度。这里有一些不同移动方式典型速度范围的指导方针:

  • 步行速度通常最高能达到 2.5 米每秒(5.6 mph, 9 km/h)
  • 跑步速度范围从 2.5 到 7.5 米每秒(5.6 – 16.8 mph, 9 – 27 km/h)
  • 骑行速度范围从 3 到 12 米每秒(6.7 – 26.8 mph, 10.8 – 43.2 km/h)
  • 汽车的速度可以超过 100 米每秒(220 mph, 360 km/h)

或者,你可能会使用位置数据来改变 UI,取决于现在的位置是否在一片水域。

if currentLocation.intersects(waterRegion) {
        if activity.walking {
        print("🏊‍")
        } else if activity.automotive {
        print("🚢")
        }
        }
        

然而,位置数据应该只在绝对必要的时候再查询——当你需要查询时,也应该尽量少的查询,比如只检测显著的位置改变。这样的原因是,获取位置数据要求使用 GPS 且/或移动网络,它们都非常耗电。


CMMotionActivityManager 是 Core Motion 里那些好用 API 的其中一个,你可以使用它来构造沉浸般的、快速响应的应用。

如果你还没有考虑过将设备运动信息纳入应用的潜力(或者可能你很久没有关注过 Core Motion),你可能会被它能做到的事情惊讶到。

NSDataAsset

作者 Mattt
2018年8月26日 00:00

在 Web 的世界里,速度不是一种奢求;它事关生死。

近年来的用户研究表明,页面加载中 任何 可以察觉到的延迟 —— 即大于 400 毫秒(字面意义上的“一眨眼的功夫”) —— 都会对转化率和参与率产生负面影响。网页加载时每多花一秒,就会多 10% 的用户返回或者关闭这个页面。

对于谷歌、亚马逊和 Netflix 这样的大型的互联网公司而言,加载时多花一秒钟就意味着损失 数十亿 美元的年收入。所以那些公司投入如此多的工程努力来让网页更快,也没有什么奇怪的了。

有很多加速网络请求的技术:压缩和流技术、缓存和预加载、连接池和多路复用、延迟和后台运行。然而,还有一种比它们优先级更高,效果更好的优化策略:压根就不发请求

在这个方面,App 凭借先下载后使用的特点,拥有传统网页所不具备的独特优势。在这一周的 NSHipster 里,我们将展示如何以一种非传统的方式使用 Asset Catalog 来改善你的 App 的首次启动体验。


Asset Catalog 允许你根据当前设备的特性来组织资源文件。对于一个给定的图片,你可以根据设备(iPhone、iPad、Apple Watch、Apple TV、Mac)、屏幕分辨率(@2x / @3x)或者色域(sRGB / P3),提供不同的文件。对于其他类型的 asset,你可能根据可用内存或者 Metal 版本的不同而提供不同的文件。请求 asset 时仅需提供名字,最合适的那个资源就会自动返回。

除了提供更简便的 API,Asset Catalog 还允许 App 使用 app thinning 为每个用户设备提供一个经过优化的更小的安装包。

图片是最常见的 Asset 类型,但是从 iOS 9 和 macOS El Capitan 开始,JSON、XML 和其他数据文件之类的资源也可以通过 NSDataAsset 这种有趣的方式参与进来。

如何使用 Asset Catalog 存储和获取数据

举个例子,让我们想象一个用于创建数字调色板的 iOS App。

为了区分不同深浅的灰色,我们可能会加载一个颜色和对应名字的列表。通常情况下,我们可能会在第一次启动时从服务器下载这个列表,但是如果恶劣的网络环境限制了 App 的功能,就会导致很差的用户体验。既然它是一个相对静态的数据集,为什么不以一种 Asset Catalog 形式将它添加到 app bundle 中?

步骤 1:向 Asset Catalog 中添加 New Data Set

当你在 Xcode 中新建一个 app 项目时,它会自动生成一个 Asset Catalog。在项目导航(Project navigator)中选中 Assets.xcassets,打开 Asset Catalog 编辑器。点击左下方的 + 图标,然后选择 “New Data Set”。

asset add-new-data-set.png

这样会在 Assets.xcassets 下新建一个后缀名为 .dataset 的子目录。

步骤2:添加数据文件

打开 Finder,找到数据文件,把它拖拽到 Xcode 中 data set asset 的空白处。

asset asset-catalog-any-any-universal.png

当你这么做时,Xcode 会把那个文件复制到 .dataset 子目录,并将它的文件名和 通用类型标识符(Universal Type Identifier) 更新到 contents.json 元数据文件。

{
        "info": {
        "version": 1,
        "author": "xcode"
        },
        "data": [
        {
        "idiom": "universal",
        "filename": "colors.json",
        "universal-type-identifier": "public.json"
        }
        ]
        }
        

步骤3:使用 NSDataAsset 访问数据

现在你可以使用如下代码访问文件的数据:

guard let asset = NSDataAsset(name: "NamedColors") else {
        fatalError("Missing data asset: NamedColors")
        }
        let data = asset.data
        

对于我们颜色 App,我们可能在一个 view controller 的 viewDidLoad() 方法中调用上面的代码,然后解码返回的数据,获取 model 对象的数组,并展示在一个 table view 上。

let decoder = JSONDecoder()
        self.colors = try! decoder.decode([NamedColor].self, from: asset.data)
        

混合一下

Data set 通常无法从 Asset Catalog 的 app thinning 特性中获益(例如,大部分的 JSON 文件都不太关心设备所支持的 Metal 版本)。

但是对于我们的调色板 App,我们可能为支持广色域显示的设备提供不同的颜色列表。

为了做到这一点,在 Asset Catalog 编辑器的侧边栏选中刚才的 asset,然后点击 Attributes Inspector 下名为 Gamut 的下拉控件。

asset select-color-gamut.png

为每个色域提供定制的数据文件后,contents.json 元数据文件应该看起来像这样:

{
        "info": {
        "version": 1,
        "author": "xcode"
        },
        "data": [
        {
        "idiom": "universal",
        "filename": "colors-srgb.json",
        "universal-type-identifier": "public.json",
        "display-gamut": "sRGB"
        },
        {
        "idiom": "universal",
        "filename": "colors-p3.json",
        "universal-type-identifier": "public.json",
        "display-gamut": "display-P3"
        }
        ]
        }
        

保鲜一下

使用 Asset Catalog 存储和获取数据是非常简单的。真正困难 —— 并最终更重要 —— 的是保持数据的更新。

使用 curlrsyncsftp、Dropbox、BitTorrent 或 Filecoin 刷新数据。从一个 shell 脚本开始(如果你喜欢,可以在 Xcode Build Phase 中调用它)。将它添加到你的 MakefileRakefileFastfile,或者你的编译系统所要求的任何地方。将这个任务分配给 Jenkins、Travis 或者某个烦人的实习生。使用定制的 Slack integration 或者 Siri Shortcuts 触发它,这样你就可以用随意的一句 “Hey Siri,在数据变得太旧之前更新一下”,让你的同事大吃一惊。

注意,当你决定同步你的数据时,一定要确保它是自动化的,而且是你发布过程的一部分。

下面是一个 shell 脚本示例,你可以运行它来使用 curl 下载最新的数据文件:

#!/bin/sh
        CURL='/usr/bin/curl'
        URL='https://example.com/path/to/data.json'
        OUTPUT='./Assets.xcassets/Colors.dataset/data.json'
        $CURL -fsSL -o $OUTPUT $URL
        

封装一下

虽然 Assets Catalog 会对 image asset 执行无损压缩,但没有任何文档、Xcode 帮助或 WWDC session 指出 data asset 上也存在这种优化(至少目前没有)。

当 data asset 的文件大小大于,比如说几百 KB 时,你就要考虑使用压缩了。JSON、CSV 和 XML 之类的文本文件尤其如此,它们通常可以被压缩到原始大小的 60% - 80%。

我们可以将 curl 的输出发送给 gzip,然后再写到我们的文件,从而为我们之前的 shell 脚本添加压缩功能。

#!/bin/sh
        CURL='/usr/bin/curl'
        GZIP='/usr/bin/gzip'
        URL='https://example.com/path/to/data.json'
        OUTPUT='./Assets.xcassets/Colors.dataset/data.json.gz'
        $CURL -fsSL $URL | $GZIP -c > $OUTPUT
        

If you do adopt compression, make sure that the "universal-type-identifier" field reflects this:

如果你使用了压缩,请确保 "universal-type-identifier" 字段体现了这一点:

{
        "info": {
        "version": 1,
        "author": "xcode"
        },
        "data": [
        {
        "idiom": "universal",
        "filename": "colors.json.gz",
        "universal-type-identifier": "org.gnu.gnu-zip-archive"
        }
        ]
        }
        

在客户端上,你使用 asset catalog 之前需要先解压数据。如果有 Gzip 模块,你可能会做以下事情:

do {
        let data = try Gzip.decompress(data: asset.data)
        } catch {
        fatalError(error.localizedDescription)
        }
        

或者,如果你会在 App 中反复地这么做,那么可以在 NSDataAsset 的扩展中创建一个便利方法:

extension NSDataAsset {
        func decompressedData() throws -> Data {
        return try Gzip.decompress(data: self.data)
        }
        }
        

尽管人们容易认为所有用户都享受着快速的、无处不在的 WiFi 和 LTE 网络,但这并不适用于所有人,也不适用于所有时段。

花点时间看看你的 App 在启动时发出的网络请求,然后考虑哪些可能从预加载中受益。给人留下好的第一印象可能意味着你的 App 是被长期地积极地使用着,而不是几秒钟之后就被删除。

密码规则 / UITextInputPasswordRules

作者 Mattt
2018年7月23日 00:00

也难怪 hipster 们着迷于工艺品和手工制品。不管是一片厚切鳄梨吐司、一瓶限量(非乳制)姜黄奶或一杯完美的手冲咖啡——其中的人情味是无法替代的。

相反,好密码和工艺品截然不同。密码应该完全没有任何意义,除非它是一个 90 年代骇客电影的标题或者一个密室逃脱游戏的答案。

有了 iOS 12 和 macOS Mojave 中的 Safari,生成可以想象到的最强、最没有意义、最难猜到的密码从未如此简单——这都要感谢一些新功能。


理想的密码策略非常简单:强制要求最少字符数(至少 8 位)并且允许长密码(64 位或者更多)。

其他更复杂的策略,像预置的安全问题、周期性的失效密码或者强制要求一些奇怪的符号,只不过让这些策略想要保护人感到厌烦。

但是不要太相信我说的话——我不是安全专家

相对的,请查看美国国家标准技术研究所最新发布(2017 年 6 月)的 Digital Identity Guidelines

好消息是越来越多的公司和组织开始注意安全性最佳实践了。坏消息则是改变这些事情需要进行一系列影响数百万人的大范围数据改动。事实上前面说到的安全性反面模式并不会很快消失,因为公司和政府做任何事情都需要花很久的时间。

自动式强密码

Safari 的自动填充从 iOS 8 起就可以生成密码了,但是它有一个缺点就是不能保证生成的密码符合某些服务的要求。

Apple 通过 iOS 12 和 macOS Mojave 里 Safari 中的自动式强密码功能来解决这个问题。

WebKit 工程师 Daniel Bates 在 3 月 1 日给 WHATWG 提交了这个提案。6 月 6 日,WebKit 团队发布了 Safari Technology Preview 58,使用新属性 passwordrules 来支持强密码生成。同时,WWDC 发布了 iOS 12 beta SDK,包括新的 UITextInputPasswordRules API,还有验证码自动输入和联合身份验证等其他一些密码管理功能。

密码规则

密码规则就像是密码生成器的配方。根据一些简单的规则,密码生成器就可以随机生成满足服务提供方需求的新密码。

密码规则由一个或多个键值对组成:

required: lower; required: upper; required: digit; allowed: ascii-printable; max-consecutive: 3;

每个规则可以指定下列键:

  • required: 需要的字符类型
  • allowed: 允许使用的字符类型
  • max-consecutive: 允许字符连续出现次数的最大值
  • minlength: 密码最小长度
  • maxlength: 密码最大长度

requiredallowed 键使用下面列出的字符类别作为值。max-consecutiveminlengthmaxlength 使用非负整数作为值。

字符类别

requiredallowed 键可以使用下面的字符类别作为值:

  • upper (A-Z)
  • lower (a-z)
  • digits (0-9)
  • special (-~!@#$%^&\*\_+=`|(){}[:;"'<>,.? ] 和空格)
  • ascii-printable (U+0020 — 007f)
  • unicode (U+0 — 10FFFF)

除了这些预置字符类别,还可以用方括号包住 ASCII 字符来指定自定义字符类别(比如 [abc])。


Apple 的 Password Rules Validation Tool 让你可以对不同的规则进行实验,并得到实时的结果反馈。甚至可以生成并下载上千个密码用来开发和测试!

Password Rules Validation Tool

更多有关于密码规则的语法,请查看 Apple 的文档「Customizing Password AutoFill Rules」


指定密码规则

在 iOS 上,给 UITextFieldpasswordRules 属性设置一个 UITextInputPasswordRules 对象(同时也应该将 textContentType 属性设置为 .newPassword):

let newPasswordTextField = UITextField()
        newPasswordTextField.textContentType = .newPassword
        newPasswordTextField.passwordRules = UITextInputPasswordRules(descriptor: "required: upper; required: lower; required: digit; max-consecutive: 2; minlength: 8;")
        

在网页上,设置 <input> 元素(且 type="password")的 passwordrules 属性:

<input type="password" passwordrules="required: upper; required: lower; required: special; max-consecutive: 3;"/>
        

如果没有指定,默认的密码规则是 allowed: ascii-printable。如果表单中有密码验证区域,它的密码规则会从上一个区域继承下来。

在 Swift 中生成密码规则

不光是只有你会觉得直接使用没有良好抽象的字符串格式令人感到不安。

下面是一种将密码规则封装成 Swift API 的方式(可以作为 Swift package 获取):

enum PasswordRule {
        enum CharacterClass {
        case upper, lower, digits, special, asciiPrintable, unicode
        case custom(Set<Character>)
        }
        case required(CharacterClass)
        case allowed(CharacterClass)
        case maxConsecutive(UInt)
        case minLength(UInt)
        case maxLength(UInt)
        }
        extension PasswordRule: CustomStringConvertible {
        var description: String {
        switch self {
        case .required(let characterClass):
        return "required: \(characterClass)"
        case .allowed(let characterClass):
        return "allowed: \(characterClass)"
        case .maxConsecutive(let length):
        return "max-consecutive: \(length)"
        case .minLength(let length):
        return "minlength: \(length)"
        case .maxLength(let length):
        return "maxlength: \(length)"
        }
        }
        }
        extension PasswordRule.CharacterClass: CustomStringConvertible {
        var description: String {
        switch self {
        case .upper: return "upper"
        case .lower: return "lower"
        case .digits: return "digits"
        case .special: return "special"
        case .asciiPrintable: return "ascii-printable"
        case .unicode: return "unicode"
        case .custom(let characters):
        return "[" + String(characters) + "]"
        }
        }
        }
        

有了这个,我们就可以在代码里指定一些规则,然后用它们生成有效的密码规则语法字符串:

let rules: [PasswordRule] = [ .required(.upper),
        .required(.lower),
        .required(.special),
        .minLength(20) ]
        let descriptor = rules.map{ "\($0.description);" }
        .joined(separator: " ")
        // "required: upper; required: lower; required: special; max-consecutive: 3;"
        

只要你愿意,你甚至可以扩展 UITextInputPasswordRules 给它添加一个接收 PasswordRule 数组的 convenience initializer。

extension UITextInputPasswordRules {
        convenience init(rules: [PasswordRule]) {
        let descriptor = rules.map{ $0.description }
        .joined(separator: "; ")
        self.init(descriptor: descriptor)
        }
        }
        

如果你是一个在个人认证信息上非常有感情的人,喜欢在密码输入区域中的小圆点后面输入你的大学、小狗或者最喜欢的运动团队,请考虑不要再这样做了。

就我个人来说,我无法想象没有密码管理器的日子。当你知道任何时候你都能访问到你需要的信息,并且只有你能访问到时,你的心灵将会获得极大的平静。

从现在开始改变,你就能完全利用上在之后今年发布的 iOS 12 和 macOS Mojave 的 Safari 中的这些改进。

❌
❌