普通视图

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

Language Server Protocol

作者 Mattt
2018年11月5日 00:00

上个月,苹果公司 在 Swift.org 论坛上宣布,正在着手为 Swift 和 C 语言支持 Language Server Protocol(语言服务器协议,LSP)。

对于苹果公司而言,为所有 Swift 开发者 —— 包括非苹果平台上的 —— 提供高质量的工具支持非常重要。我们希望与开源社区合作,将精力集中在构建 Xcode 和其他编辑器、其他平台可以共享的公共基础设施上。为实现这一目标,[……],我们决定支持 LSP。

Argyrios Kyrtzidis,2018 年 10 月 15 日

这可能是苹果自 2014 年将 Swift 作为开源软件发布以来,为 Swift 做出的最重要的决定。 这对于 APP 开发者来说是一件大事,对于其他平台上的 Swift 开发者来说更是一件大事。

为了理解其中的原因,本周的文章将研究 Language Server Protocol 解决了什么问题,它是如何工作的,以及它的长期影响可能是什么。


想象这样一个矩阵,每一行表示不同的编程语言(Swift、JavaScript、Ruby、Python 等),每一列表示不同的代码编辑器(Xcode、Visual Studio、Vim、Atom 等),这样每个单元格表示特定编辑器对一种语言的支持级别。

然后,你就发现各种组合形成了一种支离破碎的兼容。有些编辑器和部分语言深度集成,但除此之外几乎什么都干不了;其他编辑器则比较通用,对很多语言都提供了基本的支持。(IDE 这个术语通常用来描述前者。)

举个奇葩的例子:你不用 Xcode 来开发 APP,却偏用来干其他事情。

为了更好地支持某一特定的语言,编辑器必须编写一些集成代码(integration code)—— 要么直接写在项目里,要么通过插件。由于不同语言和编辑器的实现机制不一样,因此比方说 Vim 改进了对 Ruby 支持,但这并不能让它更好地支持 Python,也不能让 Ruby 在 Atom 上运行地更好。最终的结果是:大量精力浪费在了不同技术的兼容上。

我们上面描述的情况通常被称为 M × N 问题,即最终的集成方案数量为编译器数量 M 与语言数量 N 的乘积。Language Server Protocol 所做的事情就是将 M × N 问题变成 M + N 问题

编辑器不必实现对每种语言的支持,只需支持 LSP 即可。之后,它就能同等程度地支持所有支持 LSP 的语言。

Language Server Protocol 为支持的语言提供了一套通用的功能集,包括:

  • 语法高亮(Syntax Highlighting)
  • 自动格式化(Automatic Formatting)
  • 自动补全(Autocomplete)
  • 语法(Syntax)
  • 工具提示(Tooltips)
  • 内联诊断(Inline Diagnostics)
  • 跳转到定义(Jump to Definition)
  • 项目内查找引用(Find References in Project)
  • 高级文本和符号搜索(Advanced Text and Symbol Search)

各种工具和编辑器可以将精力用于提升可用性和提供更高级的功能,而不是为每种新技术再造个轮子。

Language Server Protocol 的工作原理

如果你是一个 iOS 程序员,那么一定很熟悉 serverprotocol 这两个术语在 Web 应用程序的 HTTP + JSON 通信场景下的含义。实际上 Language Server Protocol 差不多也是这么工作的。

对于 LSP,client 是指编辑器 —— 或者更宽泛一点,是指工具,server 是指本地独立进程里运行的一个外部程序。

至于名字中包含 protocol,是因为 LSP 类似于一个精简版的 HTTP:

  • 每个消息都由报头部分和内容部分组成。
  • 报头部分包含一个必填的 Content-Length 字段,用于说明内容部分的大小(以字节为单位),以及一个可选的 Content-Type 字段(默认值为 application/vscode-jsonrpc; charset=utf-8)。
  • 内容部分使用 JSON-RPC 描述请求、响应和通知的结构。

每当工具中发生了什么事情,比如用户需要跳转到符号的定义,工具就会向 server 发送一个请求。server 接收到该请求,然后返回适当的响应。

例如,假设用户在支持 Language Server Protocol 的类 Xcode 编辑器中打开以下 Swift 代码:

class Parent {}
        class Child: Parent {}
        

当用户按住 后点击第二行继承位置的 Parent,编辑器跳转到第一行定义 Parent 的位置。

以下是 LSP 如何在幕后实现这种交互:

首先,当用户打开 Swift 代码时,若 Swift language server 并未运行,编辑器将在一个独立进程中启动它,并执行一些额外的配置。

当用户执行 “跳转到定义(jump to definition)” 指令时,编辑器向 Swift language server 发送以下请求:

{
        "jsonrpc": "2.0",
        "id": 1,
        "method": "textDocument/definition",
        "params": {
        "textDocument": {
        "uri": "file:///Users/NSHipster/Example.swift"
        },
        "position": {
        "line": 1,
        "character": 13
        }
        }
        }
        

收到这个请求后,Swift language server 使用 SourceKit 等编译器工具来标识相应的代码实体,并在代码的上一行找到其声明的位置。然后 language server 用以下消息进行响应:

{
        "jsonrpc": "2.0",
        "id": 1,
        "result": {
        "uri": "file:///Users/NSHipster/Example.swift",
        "range": {
        "start": {
        "line": 0,
        "character": 6
        },
        "end": {
        "line": 0,
        "character": 12
        }
        }
        }
        }
        

最后,编辑器导航到文件(在本例中,该文件已经打开),将光标移动到该范围,并高亮显示出来。

这种方法的美妙之处在于,编辑器完成所有这些操作时,除了 .swift 文件与 Swift 代码相关以外,对 Swift 编程语言一无所知。编辑器需要做的就是与 language server 对话并更新 UI。而且编辑器知道如何做到这一点后,就可以遵循相同的过程,与任何带有 language server 的语言所编写的代码进行交互。

Clang / LLVM 里的 Language Server Protocol

如果你觉得之前的 M + N 图有点眼熟,那可能是因为 LLVM 也采用了同样的方法。

LLVM 的核心是中间表示(intermediate representation,IR)。LLVM 所支持的语言使用 编译器前端(compiler frontend) 生成 IR,再使用 编译器后端(compiler backend) 将 IR 生成所支持平台的机器码。

Clang 是 C 语言的 LLVM 编译器前端。Swift 与 Objective-C 的互操作性(inter-operability)就是靠它实现的。在最近的 5.0.0 版本中,Clang 添加了一个名为 Clangd 的新工具,它是 LLVM 对 Language Server Protocol 的实现。

2018 年 4 月,苹果公司向 LLVM 邮件组宣布,将把开发的重心从 libclang 转向 Clangd,以其作为创建交互工具的主要方式。

现在你可能会想,“那又怎样?” 苹果公司是 LLVM 项目最重要的支持者之一,该项目创始人 Chris Lattner 已经在苹果公司工作了十多年。苹果公司决定从不透明的 Clang 工具切换到另一个,似乎是一个实现细节了(可以这么说)。

这个官宣很有趣的一点是,Clangd 似乎完全是在苹果以外开发的,谷歌和其他公司也做出了重大贡献。这个官宣标志着未来工具开发方向的重大转变 —— 六个月后 Swift.org 论坛将证实这一点。

苹果支持 Language Server Protocol 的潜在影响

根据苹果公司 10 月份发布的 LSP 公告,我们预计在未来几周内(撰写本文时,最早 11 月中旬)将看到该项目的首批代码。

要感受这些发展的全部影响还需要一些时间,但请相信我:你的耐心是值得的。我相信以下是 LSP 在未来几个月和几年将会发生的一些事情。

Swift 变成一种更加通用的编程语言

虽然 Swift 主要用于 APP 开发,但它从一开始就被设计成一种功能强大的通用编程语言。在 Swift for TensorFlowSwiftNIO 和其他项目中,我们正开始看到 Swift 承诺的在 App Store 之外的使用。

到目前为止,阻碍 Swift 被主流采用的最大因素之一是它对 Xcode 的依赖。

人们会有很多质疑,当有那么多优秀的、门槛低很多的替代方案可选的情况下,为什么还要让 Web 开发者或机器学习工程师仅仅为了尝试 Swift 而去下载 Xcode?支持 Language Server Protocol 可以让苹果生态圈以外的人更容易地使用他们熟悉工具去感受 Swift。

Xcode 变得更好

支持 LSP 不仅仅是让 Swift 在其他编辑器中运行地更好,Xcode 也将受益匪浅。

看看苹果 Swift 项目负责人 Ted Kremenek 的 这篇论坛帖子:

[Argyrios] 所描述的 LSP 服务将比今天的 SourceKit 更强大。

LSP 对 Xcode 团队来说是一个机遇,让他们使用一种新的方法实现 Swift 集成,并可以将其应用在语言和工具自 1.0 版本发布以来四年中的所有改进上。

Xcode(最终)变得更强大

LSP 的好处并不限于 Swift 和 Objective-C,Argyrios 在那个帖子的另一个留言中指出

Xcode 使用我们新的 LSP 服务,这意味着它也可以使用其他 LSP 服务,我们对此很感兴趣,不过目前暂无具体计划。

目前的工作重点是改进 Swift。但是,一旦实现了这一点,就应该能相对简单地将这些优化转移到支持 LSP 的其他语言中。


软件的架构反映了创建它的组织的结构和价值。在某种程度上,反之亦然。

通过让 Xcode 支持开放的 Language Server Protocol 标准,苹果正在履行其承诺,让 Swift 成功应用于苹果生态体系之外。我认为这是可行的:工具(或缺少工具)通常是技术获得人心的关键决定因素。但或许更重要的是,我认为这一决定表明,公司内部(至少是一小部分)对合作和透明度的意愿有所增强。

iOS 12

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

如果你去看今年的 WWDC Keynote 演讲,你就知道 iOS 12 的这些比较重大的的新特性: Siri Shortcuts, ARKit 2, and Core ML 2 — 更不用提风传很久的炸弹:代号为 “Marzipan” 的 iOS/Mac 桥接件终于提前透露了出来。

另外如果你看了今年的这一期 Platforms State of the Union session, 你会发现一些虽然没那么光芒四射但依旧很令人兴奋技术: 像自定义用户通知 UI, 和更新了的 Network自然语言(Natural Language) 框架。

但在 NSHipster , 我们感兴趣的是那些最详细最细微的变化 (我们可以说, 晦涩的?) 也是最终加起来却又能给我们每天的工作带来很大影响的变化。

今年的 iOS 12 版本说明Foundation 版本说明涵盖了很多这样的变化,但是它们还没有展开故事的全貌。 正因如此,你还需要深入挖掘。

为了庆祝 iOS 12 在这周的发布, 我们把通过iOS 11.4 到 12 的 API 的一些差异 分享给大家(尽管 API 已经列出来了,但是他们还没有正式文档的说明,所以记得小心使用)。


为更重要的请求在网络流量里排出优先级

你听过 iOS 里的 Fast Lane(快车道)么? 并不是 Google 的 fastlane。 也不是 Cisco 的 IOS

Fast Lane (或者写成 Fastlane?) 是一种能根据网络请求的类型(例如音频、视频或后台数据)来优化无线网络流量的一个机制。

它原本是只有 Cisco 路由器(Cisco 路由器承载了互联网一半的流量)专有的一个技术,里面包含了一些 Wi-Fi 的标准, 例如快速漫游的 802.11r、 辅助漫游的 802.11k 和无线配置的 802.11v

多亏了 2015 年 Apple 和 Cisco 的合作, iOS 开发者可以通过给网络连接提供一个服务类型(QoS 标记) 来用到这个技术 (很多高层的 API 可以自动地帮你实现这个工作)。

在 iOS 12 的新 API 里,你可以把 URLRequest 对象networkServiceType 设置为 NSURLNetworkServiceTypeResponsiveData,从而让一些时间敏感的操作优先请求。

import Foundation
        let url = URL(string: "https://example.com/checkout")!
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.networkServiceType = .responsiveData // Prioritize
        URLSession.shared.dataTask(with: request) {
        (data, response, error) in
        // ...
        }
        

这个选项还没有文档说明, 但是在 WWDC 2018 Session 714: “Optimizing Your App for Today’s Internet” 的向导里,苹果工程师已经提到了。 不过只有最需要极力减少请求时间的时候才能谨慎地使用这个特性, 例如他们提到了一个场景就是一些购物应用的收银台页面可以使用, 当然你也可以推断出一些其他的使用案例。

在后台读取 NFC 标签

在 WWDC 2018 里被排着队问到的最多的一个问题是 NSUserActivity 加入的 ndefMessagePayload 属性, 而彼时苹果工程师在 Lab Session 回答的最多的也是“无可奉告”。

但是重重迷雾终于在上周的 iPhone XS, iPhone XS Max 和 iPhone XR 的发布会上被揭开。 这些设备支持在后台读取 NFC 标签, 而且如果你在最新的设备上跑着 iOS 12, 你将可以 — 干这些 — 可以不需要别的设置, 就通过扫到的标签来启动 App、打电话或打开链接。 为了防止误激活, 这个只在 iPhone 是解锁状态下才工作, 处于飞行模式、使用 Apple Pay 中和使用相机的时候不能工作。

有了 NFC 集成,通过提供一个更顺畅与真实世界连接的途径,而不是堕落到去扫二维码(在中国非常流行,但是被世界其他地方被忽视), Apple 终于可以完全实现 2013 对于 BLE 的承诺了。

可能 NFC 和 iBeacon 最适用场景还是你在博物馆参观, 站在展品面前想了解更多信息的时候, 只需要把 iPhone 放到信息牌上就能实现。

实现类似功能需要 App 有个特别的 entitlements, 设置相关的域和其他一些相关配置 — 还有一些你需要实际实现的 API。 好在,Apple 还提供了一些过程中的扩展文档, 包括一个示例项目这篇文章.

根据电话号码和邮件地址匹配联系人

Contacts framework 是在 iOS 9 和 macOS El Capitan 作为 AddressBook framework 的更现代的替代版本出现的。

截止目前, 你只能通过姓名或者唯一编码(identifier)搜索联系人, 但是在 iOS 12 里, 你能用CNContact 类的 predicateForContacts(matching:)predicateForContacts(matchingEmailAddress:) 来构造谓词(predicate)用以匹配手机号码与邮件地址。

例如, 如果我们想要依照一组手机号和邮件地址取出所有匹配到的姓与名, 你可以创建一个 CNContactFetchRequest, 用 “AND” 来把连个子谓词(subpredicate)组合一个复合谓词,而后传给当前 CNContactStore 对象的 enumerateContacts(with:) 方法。

import Contacts
        let phoneNumber = CNPhoneNumber(stringValue: "+1 555 555 1234")
        let phoneNumberPredicate = CNContact.predicateForContacts(matching: phoneNumber)
        let emailPredicate = CNContact.predicateForContacts(matchingEmailAddress: "johnny@example.com")
        var fetchRequest = CNContactFetchRequest(keysToFetch: [
        CNContactGivenNameKey as CNKeyDescriptor,
        CNContactFamilyNameKey as CNKeyDescriptor
        ])
        fetchRequest.predicate =
        NSCompoundPredicate(andPredicateWithSubpredicates: [
        phoneNumberPredicate,
        emailPredicate
        ])
        let store = CNContactStore()
        try store.enumerateContacts(with: fetchRequest) { (contact, _) in
        // ...
        }
        

在空中更新位置信息

很多飞行员很喜欢用 iPad 来导航和制定飞行计划。 如果你的 App 是用来装备给在天上驾驶员座舱里的老兄们的话, 你就会对 iOS 12 里 CLLocationManager 的新特性感到兴奋。

activityType 属性 已经出来有一段时间了,但是针对于 CLLocationManager 的配置却鲜有人知。 如果你用这个 CLLocationManager 在一段时间内追踪位置的变化, 通过一些唾手可得的优化, 你就能知道用户在以什么行为模式运动。 但目前,运动变化的种类被严格限制在正在 驾驶类走路/跑步/骑行类其他类这三个领域。 但是在 iOS 12 里,你可以指定飞行活动类来让你的运动追踪算法飞起来!

import CoreLocation
        let manager = CLLocationManager()
        manager.activityType = .airborne // ✈️
        

检测平放设备朝向

你是否想检测设备是否平放在一个表面上, 但是只能勉强地使用 两个条件 来检测? 好消息是,在 iOS 12 里, 新增了一个 isFlat 的方便属性。

import UIKit
        // iOS 12+
        UIDevice.current.orientation.isFlat
        // iOS <= 11.4
        UIDevice.current.orientation == .faceUp ||
        UIDevice.current.orientation == .faceDown
        

新密码的自动输入与一次性验证码在文本框里的输入

尽管 Apple 为了在让用户在设备上愉悦地输入做了长期的英雄般卓越的工作, 但是有一些事实还是不能改变: 在一个毫无触感的光滑玻璃上输入总是比在一个合适的硬件键盘上输入要苍白 (尽管依然要吐槽新 MacBook 的键盘)。

为了减少文本输入之类的苦差事, iOS 10 为一些服从 UITextInputTraits 协议的控件(分别是 UITextFieldUITextView) 提供了 textContentType 属性。 只要提供一个具体的枚举类型,你就指定了这个控件的语义类型, 就可以依据当前用户的信息,自动填入名字或者地址类的信息。

iOS 12 和 tvOS 12 拓展了这个枚举,增加了 UITextContentTypeNewPasswordUITextContentTypeOneTimeCode 类型。

当你同时指定 .newPasswordpasswordRules 属性的时候, 密码自动填写功能(Password AutoFill)就能根据系统登录密码的要求自动生成一个新的密码。

textField.textContentType = .newPassword
        textField.passwordRules = .init(descriptor:
        "allowed: ascii-printable; minlength: 8;"
        )
        

当你指定了 .oneTimeCode 内容类型的时候, 文本框能够自动地转发短信的二次验证码。

textField.textContentType = .oneTimeCode
        

我们这一期的 iOS 12 代码 diff 探险就在这里结束了。 当然,这是一次庞大的更新, 我们期待在接下来的几周再讲解更多更深的新 API。

对之后的内容有任何建议? 来 Twitter 联系我们!

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 中的这些改进。

❌
❌