阅读视图

发现新文章,点击刷新页面。

属性修饰器

作者:Mattt,原文链接,原文日期:2019-06-24
译者:ericchuhong;校对:Desgard,numbbbbbSketchK;定稿:Pancf

几年前,我们 会说 “at 符号”(@)——以及方括号和可笑的长方法名称——是 Objective-C 的特性,正如括号之于 Lisp 或者标点之于 Perl

然后 Swift 来了,并用它来结束这些古怪小 🥨 图案一样的字形。或者说我们本以为会这样。

一开始,Swift 中的 @ 只用在和 Objective-C 的混编中:@IBAction@NSCopying@UIApplicationMain等等。但之后 Swift 扩展出了越来越多的带有 @ 前缀的 属性

我们在 WWDC 2019 上第一次看到了 Swift 5.1 和 SwiftUI 的同时公布。并且随着每一张“令人兴奋”的幻灯片出现了一个个前所未有的属性:@State@Binding@EnvironmentObject……

我们看到了Swift的未来,它充满了 @ 符号。


等 SwiftUI 逐步成熟起来,我们才会深入介绍它。

本周,我们想仔细看看 SwiftUI 的一个关键语言特性——可能会对 Swift 5.1 之前版本产生最大影响的东西:属性修饰器


关于 属性 代理 修饰器

属性修饰器是在 2019 年 3 月第一次 在 Swift 论坛首次出现——SwiftUI 公布的前一个月。

在开始的时候,Swift 核心团队成员 Douglas Gregor 将它作为用户常用功能特性的一个统称(当时称为 “属性代理”),像有 lazy 关键字之类的。

懒惰是程序员的美德,这种普遍适用的功能是周到设计决策的特征,这让 Swift 成为一种很好用的语言。当一个属性被声明为 lazy 时,它推迟初始化其默认值,直到第一次访问才进行初始化。例如,你可以自己尝试实现这样的功能,使用一个私有属性,它需通过计算后才行被访问。而单单一个 lazy 关键字就可以让所有这些都变得没有必要。

struct <#Structure#> {
// 使用 lazy 关键字进行属性延迟初始化
lazy var deferred = <#...#>

// 没有 lazy 关键字的等效行为
private var _deferred: <#Type#>?
var deferred: <#Type#> {
get {
if let value = _deferred { return value }
let initialValue = <#...#>
_deferred = initialValue
return initialValue
}

set {
_deferred = newValue
}
}
}

SE-0258: 属性修饰器 目前正在进行第三次审核(预定于昨天结束,就在发布的时候), 并且承诺开放像 lazy 这样的功能,以便库作者可以自己实现类似的功能。

由于这个提案在其设计和实现上的阐述非常出色,我们这里就不做更多的解释了。我们不妨把重点放在别处,一起来看看这个功能为 Swift 带来了哪些新的可能——而且,在这个过程中,我们可以更好了解如何在项目使用这个新功能。

所以,供你参考,以下是新 @propertyWrapper 属性的四个潜在用例:


约束值

SE-0258 提供了大量实用案例,包括了 @Lazy@Atomic@ThreadSpecific@Box。但最让我们兴奋的是那个关于 @Constrained 的属性修饰器。

Swift 标准库提供了 精确、高性能的浮点数类型,并且你可以拥有任何想要的精度——只要它是 3264(或 80)位长度。

如果你想要实现自定义浮点数类型,而且有强制要求有效值范围,这从 Swift 3 开始已经成为可能。但是这样做需要遵循错综复杂的协议要求:

来自:航空学院的 Swift 数字指引

要把这么多协议实现下来工作量可不小,并且对于大多数用例,通常需要大量的工作来验证。

幸好,属性修饰器提供了一种将标准数字类型参数化的方式,同时又大大减少工作量。

实现一个限制值范围的属性修饰器

思考下面的 Clamping 结构。作为一个属性修饰器(由 @propertyWrapper 属性表示),它会自动在规定的范围内“限制”越界的值。

@propertyWrapper
struct Clamping<Value: Comparable> {
var value: Value
let range: ClosedRange<Value>

init(initialValue value: Value, _ range: ClosedRange<Value>) {
precondition(range.contains(value))
self.value = value
self.range = range
}

var wrappedValue: Value {
get { value }
set { value = min(max(range.lowerBound, newValue), range.upperBound) }
}
}

你可以使用 @Clamping 保证属性在转成模型 化学溶液中的酸度 的过程中,处于 0-14 的常规范围内。

struct Solution {
@Clamping(0...14) var pH: Double = 7.0
}

let carbonicAcid = Solution(pH: 4.68) // 在标准情况下为 1 mM

如果尝试将 pH 值设定在限制的范围之外,将得到最接近的边界值(最小值或者最大值)来代替。

let superDuperAcid = Solution(pH: -1)
superDuperAcid.pH // 0

你可以在其他属性修饰器的实现中使用属性修饰器。例如,这个 UnitInterval 属性修饰起器委托给 @Clamping,把值约束在 0 和 1 之间,包括 0 和 1。

@propertyWrapper
struct UnitInterval<Value: FloatingPoint> {
@Clamping(0...1)
var wrappedValue: Value = .zero

init(initialValue value: Value) {
self.wrappedValue = value
}
}

再比如,你可以使用 @UnitInterval 属性修饰器定义一个 RGB 的类型,用来表示红色、绿色、蓝色的百分比强度。

struct RGB {
@UnitInterval var red: Double
@UnitInterval var green: Double
@UnitInterval var blue: Double
}

let cornflowerBlue = RGB(red: 0.392, green: 0.584, blue: 0.929)

举一反三

  • 实现一个 @Positive/@NonNegative 属性装饰器,将无符号整数赋值成有符号整数类型。
  • 实现一个 @NonZero 属性装饰器,使得一个数值要么大于,要么小于 0
  • @Validated 或者 @Whitelisted/@Blacklisted 属性装饰器,约束了什么样的值可以被赋值。

转换属性赋值时的值

从用户接收文本输入是应用开发者经常头疼的问题。从无聊的字符串编码到恶意的文本字段注入攻击,开发者有太多事情需要注意。但在开发者面对的的问题中,最难以捉摸和令人困扰的是接收用户生成的内容,而且这些内容开头和结尾都带有空格。

在内容开头有一个单独的空格,可以让 URL 无效,也可以混淆日期解析器,还可能造成差一错误(off-by-one error):

import Foundation

URL(string: " https://nshipster.com") // nil (!)

ISO8601DateFormatter().date(from: " 2019-06-24") // nil (!)

let words = " Hello, world!".components(separatedBy: .whitespaces)
words.count // 3 (!)

说到用户输入,客户端经常以没留意做理由,然后把所有东西 原原本本 发送给服务器。¯\_(ツ)_/¯

当然我不是在倡导客户端应该为此负责更多处理工作,这种情况就涉及到了 Swift 属性修饰器另外一个引人注目的用例。


Foundation 框架将 trimmingCharacters(in:) 方法桥接到了 Swift 的字符串中,除了一些其他作用以外,它提供了便利的方式来裁剪掉 String 值首位两端的空格。虽然可以通过调用这个方法来保证数据健全,但是还不够便利。如果你也有过类似的经历,你肯定会想知道有没有更好的方案。

或许你找到了一种较为通用的方法,通过 willSet 属性回调来寻解脱……唯一让人不能满意的是,这个方法无法改变已经发生的事情。

struct Post {
var title: String {
willSet {
title = newValue.trimmingCharacters(in: .whitespacesAndNewlines)
/* ⚠️ 尝试在它自己的 willSet 中存储属性 'title',该属性将会被新值覆盖*/
}
}
}

从上面看,你可能想到可以用 didSet,作为解决问题的康庄大道……不过我想你马上就会想起来 Swift 里的一条规定,即 didSet 在属性初始化赋值时是不会被调用的。

struct Post {
var title: String {
// 😓 初始化期间未调用
didSet {
self.title = title.trimmingCharacters(in: .whitespacesAndNewlines)
}
}
}

在属性自己的 didSet 回调方法里面,很幸运不会再次触发回调,所以你不必担心意料之外的递归调用。

在你的坚持不懈下,你很可能用尽了一切办法……但回过头来,你发现其实并没有什么方法能够既满足人因工程学的标准,又满足性能方面的要求

如果你对此深有体会,那么恭喜你,你在这方面的探索可以到此为止了,因为属性装饰器将是这个问题的终极解决方案。

实现为字符串值裁截空格的属性修饰器

看下下面的 Trimmed 结构体,它从输入的字符串裁截了空格和换行。

import Foundation

@propertyWrapper
struct Trimmed {
private(set) var value: String = ""

var wrappedValue: String {
get { value }
set { value = newValue.trimmingCharacters(in: .whitespacesAndNewlines) }
}

init(initialValue: String) {
self.wrappedValue = initialValue
}
}

下面的代码为 Post 结构中每个 String 属性标记了 @Trimmed ,通过这种方式,任何赋值给 titlebody 的字符串值——无论是在初始化期间还是通过属性访问后——都将自动删除其开头或结尾的空格。

struct Post {
@Trimmed var title: String
@Trimmed var body: String
}

let quine = Post(title: " Swift Property Wrappers ", body: "<#...#>")
quine.title // "Swift Property Wrappers" (no leading or trailing spaces!)

quine.title = " @propertyWrapper "
quine.title // "@propertyWrapper" (still no leading or trailing spaces!)

举一反三

  • 实现一个 @Transformed 属性修饰器,它允许对输入的字符串进行 ICU 转换
  • 实现一个 @Normalized 属性修饰器,它允许一个 String 属性自定义它正规形式
  • 实现一个 @Quantized/@Rounded/@Truncated 属性修饰器,它会把数值转换到一种特定的精度(例如:向上舍入到最近的 ½ 精度),但是内部要关注到精确过程的中间值,防止连锁的舍入错误。

改变生成的等式和比较语义

这个方式取决于遵循 synthesized 协议的实现细节,并且可能会在这个功能完成之前发生改变(尽管我们希望这个方法仍然像下面所说一样继续可用)。

在 Swift 中,两个 String 值如果他们 标准等价 就会被人认为是相等。在大多数情况下,Swift 字符串的比较方式与我们的预期一致:即两个字符串包含有相同的字符就会相等,不管它是一个合成字符,还是将这个合成字符拆解成多个字符——举个例子来说,就是“é”(U+00E9 带有锐音的拉丁小写字母 E)等于“e”(U+0065 拉丁小写字母 E)+“◌́”(U+0301T 和锐音组合)。

但是,如果你在特殊的情况下需要不同的相等语义呢?例如字符串相等的时候 不区分大小写

在今天,你可以使用许多方法,利用已有的语言特性解决这个问题:

  • 要完成这个功能,你可以在 == 比较的时候用 lowercased() 做一次处理,但和其他手动处理方式一样,这种方式容易出现人为的错误。
  • 你可以创建一个包含 String 值的自定义 CaseInsensitive 类型。但你必须要完成很多额外的工作,才能把它打磨的像标准的 String 类型一样即符合人因工程学的标准,又提供完全相同的功能。
  • 虽然你可以定义一个自定义操作符 但又有什么操作符能比 == 更贴近相等的含义呢。

上面的方法并没有哪个能让人完全信服,还好在 Swift 5.1 中,属性修饰器的特性让我们拥有了一个完美的解决方案。

和文章开头提到状况一样(即实现一个自定义浮点数类型),Swift 采用面向协议的方式,将完成字符串的职责代理给一系列的更细粒度的类型(narrowly-defined types).

对于好奇心强的读者,这里是一张关系图,里面展示了在 Swift 标准库中所有字符串类型之间的关系。

来自:航空学院的 Swift 字符串指引

当你 能够 创建一个与 String 等价的自定义类型时,文档 却又强烈的建议不要这样做:

不应该再有别的类型遵循 StringProtocol 。在标准库中应当只有 StringSubstring 遵循它。

实现一个不区分大小写的属性修饰器

下面的 CaseInsensitive 类型实现了一个修饰 String/SubString 的属性修饰器。通过桥接 NSString 的 API caseInsensitiveCompare(_:)CaseInsensitive 类型符合了 Comparable 协议(本质是通过扩展的方式实现了 Equatable 协议):

import Foundation

@propertyWrapper
struct CaseInsensitive<Value: StringProtocol> {
var wrappedValue: Value
}

extension CaseInsensitive: Comparable {
private func compare(_ other: CaseInsensitive) -> ComparisonResult {
wrappedValue.caseInsensitiveCompare(other.wrappedValue)
}

static func == (lhs: CaseInsensitive, rhs: CaseInsensitive) -> Bool {
lhs.compare(rhs) == .orderedSame
}

static func < (lhs: CaseInsensitive, rhs: CaseInsensitive) -> Bool {
lhs.compare(rhs) == .orderedAscending
}

static func > (lhs: CaseInsensitive, rhs: CaseInsensitive) -> Bool {
lhs.compare(rhs) == .orderedDescending
}
}

虽然大于运算符(>可以被自动派生,我们为了优化性能应该在这里实现它,避免对底层方法 caseInsensitiveCompare 进行不必要的调用。

构造两个只是大小写不同的字符串,并且对于标准的相等检查他们会返回 false,但是在用 CaseInsensitive 对象修饰的时候返回 true

let hello: String = "hello"
let HELLO: String = "HELLO"

hello == HELLO // false
CaseInsensitive(wrappedValue: hello) == CaseInsensitive(wrappedValue: HELLO) // true

到目前为止,这个方法看起来和前文提到的方案,即创建一个包含 String 值的自定义 CaseInsensitive 类型,没什么区别。不过想要让自定义的 CaseInsensitive 类型变得和 String 一样好用,我们还需要考虑实现诸如 ExpressibleByStringLiteral 在内的其他协议,所以这才是漫漫长路的开始。

不过属性修饰器允许我们抛开这些繁琐的工作:

struct Account: Equatable {
@CaseInsensitive var name: String

init(name: String) {
$name = CaseInsensitive(wrappedValue: name)
}
}

var johnny = Account(name: "johnny")
let JOHNNY = Account(name: "JOHNNY")
let Jane = Account(name: "Jane")

johnny == JOHNNY // true
johnny == Jane // false

johnny.name == JOHNNY.name // false

johnny.name = "Johnny"
johnny.name // "Johnny"

这里,Account 对象通过 name 属性进行了一次判等,且判等的过程中不区分字母的大小写。可是当我们去获取或设置 name 属性时,它又像一个 真正的 String 值一样区分字母大小写了。

这很整洁,但这里到底发生了什么?

自 Swift 4 以后,如果某个类型里的存储属性都遵守了 Equatable 协议的话,那么编译器将自动为这个类型增加 Equatable 的能力。因为这些实现是隐式的(至少目前看起来是这样),属性修饰器是通过被封装的值进行判等的,而不是对构成属性修饰器的值判等。

// 由 Swift 编译器生成
extension Account: Equatable {
static func == (lhs: Account, rhs: Account) -> Bool {
lhs.$name == rhs.$name
}
}

举一反三

  • 定义 @CompatibilityEquivalence 属性修饰器,当修饰 String 类型的属性时,带有 "①""1" 时会被认为相等。
  • 实现一个 @Approximate 属性修饰器,来重新定义浮点数类型的相等语义 (另见 SE-0259)。
  • 实现一个 @Ranked 属性修饰器,它会带有一个函数,函数中定义了枚举值的排序;而这个排序需要符合我们通常打牌时的规则,例如牌面为 A 时,它既有可能是最大值,也可能是最小值。

审查属性访问

业务要求可能会用某些控制措施,规定谁可以访问哪些记录,或者规定一些形式表格要随着时间变换。

重申一下,类似这样的功能通常不会在 iOS 端上完成;大多数业务逻辑都是在服务端完成的,许多客户端开发者并不想与这样的业务逻辑打交道。而下面的这个例子打开了一个新的视角来看待这个问题,当然这也归功于属性修饰器的功劳。

为属性值增加版本记录

下面的 Versioned 结构体函数用作一个属性修饰器,拦截了输入的值,并在设置每个值的时候创建带时间戳的记录。

import Foundation

@propertyWrapper
struct Versioned<Value> {
private var value: Value
private(set) var timestampedValues: [(Date, Value)] = []

var wrappedValue: Value {
get { value }

set {
defer { timestampedValues.append((Date(), value)) }
value = newValue
}
}

init(initialValue value: Value) {
self.wrappedValue = value
}
}

下面是 ExpenseReport 类,它带有一个名为 state 的属性并被 @Versioned 属性修饰期所修饰。通过这种方式,我们可以回溯每一次的操作记录。

class ExpenseReport {
enum State { case submitted, received, approved, denied }

@Versioned var state: State = .submitted
}

举一反三

  • 实现一个 @Audited 属性修饰器,在每次读写属性的时候打印日志。
  • 实现一个 @Decaying 属性修饰器,它在每次值被读取的时候都会去除以一个设定的值。

不可否认的是,这个特定的示例还是暴露了属性修饰器的一些局限性:属性无法被标记为 throws当然这个问题的根源还是在 Swift 语言自身上。

由于在错误处理上的能力欠缺,属性修饰器并没有什么好办法让代码完全按照你的设想执行。例如我们想让 @Versioned 属性修饰器支持这样一个特性,即在设置 state 属性时 ,当属性被设置为 .denied 后,就不能再被设置为 .approved,针对这种场景,现有的最佳方案是 fatalError(),但在实际的生产环境中,这可就不一定了:

class ExpenseReport {
@Versioned var state: State = .submitted {
willSet {
if newValue == .approved,
$state.timestampedValues.map { $0.1 }.contains(.denied)
{
fatalError("J'Accuse!")
}
}
}
}

var tripExpenses = ExpenseReport()
tripExpenses.state = .denied
tripExpenses.state = .approved // Fatal error: "J'Accuse!"

属性修饰器的局限性还有不少,这里提到的只是其中一点。所以为了更理性的看待这个特性,文章剩下的篇幅将会说说它的局限性都体现在哪里。

局限性

受我目前的理解能力和想象能力所限,下面给出的观点可能比较主观,有可能并不是属性修饰器这个提议本身造成的。
如果你有任何好的建议或者意见,欢迎 联系我们

属性不能参与错误处理

属性不像函数,无法使用 throws 标记。

关于上面提到的问题,原本就是函数与属性之间为数不多的区别之一。由于属性同时拥有获取方法(getter)和设置方法(setter),所以在这里如何进行错误处理并没有明确的最佳实践。尤其是你需要在兼顾访问控制,自定义获取方法/设置方法和回调的状态下,还写出优雅的语句。

如上一节所示,可以通过下面两种方式来处理非法值问题:

  1. 忽略它们(静默地)
  2. fatalError() 抛出崩溃。

不论哪一种方案都不够优雅,所以如果你对这个问题有更好的解决方案,欢迎分享。

属性修饰器无法起别名

这个提议的另外一个限制就是,你不能使用属性修饰器的实例作为属性修饰器。

还记得前面提到的 UnitInterval 么?我们可以用它来限制属性值的范围在 0 到 1 之间。所以我们是不是可以用写成下面的样子呢?:

typealias UnitInterval = Clamping(0...1) // ❌

可惜这样是不被允许的。同样你也不能使用属性修饰器的实例来修饰属性。

let UnitInterval = Clamping(0...1)
struct Solution { @UnitInterval var pH: Double } // ❌

上面的代码说明一个问题,在实际使用过程中,我们可能会写出比预期多的重复代码。但考虑到这个问题的本质是计算机编程语言中值与类型是两种完全不同的东西引起的。所以从避免错误抽象的角度来看,这一小点的重复是完全可以忍受的。

属性修饰器很难组合

属性修饰器的组合不是一个可交换的操作;你声明它们的顺序影响了它们的作用顺序。

属性在进行 字符串字符串的 string inflection 操作 和 string transforms 操作会互相影响。例如下面的属性修饰器组合,它的功能是将博客文章中的 URL “slug” 属性自动格式化,但这里的问题在于将短划线替换成空格的操作和去除空格的操作会互相影响,进而导致最终的结果发生变化。

struct Post {
<#...#>
@Dasherized @Trimmed var slug: String
}

但是,要让它先发挥作用,说起来容易做起来难!尝试组合 String 值的两个属性修饰器方法失败,因为最外层修饰器影响了在最内层的修饰器类型的值。

@propertyWrapper
struct Dasherized {
private(set) var value: String = ""

var wrappedValue: String {
get { value }
set { value = newValue.replacingOccurrences(of: " ", with: "-") }
}

init(initialValue: String) {
self.wrappedValue = initialValue
}
}

struct Post {
<#...#>
@Dasherized @Trimmed var slug: String // ⚠️ 发生内部错误.
}

目前是有一个办法实现这个特性,但并不怎么优雅。关于这个问题是会在后续的版本中进行修复,还是通过文档正式说明都需要我们耐心的等待。

属性修饰器不是一等依赖类型

依赖类型 是由它的值定义的类型。例如,“一对后者比前者更大的整数”和“一个具有素数元素的数组”都是依赖类型,因为他们的类型定义取决与他们的值。

在 Swift 的类型系统里缺少对依赖类型的支持,如果想获得相关的特性需要在运行时完成。

好消息是,相比于其他语言,Swift 的属性修饰器算是第一个吃螃蟹的,不过即使这样,属性修饰器还不能算是一个完整的值依赖类型解决方案。

例如,你还是不能使用属性修饰器定义一个新类型,即使属性修饰器本身没什么毛病。

typealias pH = @Clamping(0...14) Double // ❌
func acidity(of: Chemical) -> pH {}

你也不能使用属性修饰器去注解集合中的键类型或值类型。

enum HTTP {
struct Request {
var headers: [@CaseInsensitive String: String] // ❌
}
}

这些缺点还是可以忍受的。属性修饰器非常有用,并且弥补了语言中的重要空白。

不知道属性修饰器的诞生会不会重燃大家对依赖类型的关注,当然另外一种可能是大家觉得当前的状态“也不是不能用”,也就没必要将依赖类型这个概念进一步正式化。

属性修饰器难以被文档化

突击测验:SwiftUI 框架提供了哪些可用的属性修饰器?

去吧,看下 SwiftUI 官方文档,然后试着回答。

😬

公平地讲,这种失败不是属性修饰器所特有的。

如果你的任务是明确标准库中某个 API 都需要哪些协议响应,或是在 developer.apple.com 文档中明确某个运算符都支持哪些类型时,你其实就可以考虑转行了。

随着 Swift 的复杂性不断增加,它的可理解性就会不断下降,我想没有比这更让人头疼了吧。

属性修饰器让 Swift 进一步复杂化

Swift 是一门比 Objective-C 更加 复杂的语言。自 Swift 1.0 以来,这就是一条不变的真理。

在 Swift 中有大量的 @ 前缀,从 Swift 4 提出的 @dynamicMemberLookup@dynamicCallable ,到 Swift for Tensorflow 里的 @differentiable@memberwise,即使有文档在手,这些东西也使得 Swift 的 API 越来越难理解。从这个角度来看,@propertyWrapper 无疑是加重了这个问题的严重性。

我们要如何理解这一切?(这是一个客观的真是问题,不是反问。)


好了,现在让我们总结一下这个新特性——

属性修饰器能够让开发者使用到更高层级的语言特性,而这在以前是不可能的。这个提议在提高代码安全性和降低代码复杂性上有巨大的潜力,现阶段我们只是看到了它的一些基本可能性而已。

然而,他们有所承诺,属性修饰器及其他语言特性与 SwiftUI 一起的首次亮相将给 Swift 带来了巨大的变化。果不其然,如他们之前承诺的一样,属性修饰器和其他的新特性随着 SwitUI 在这个夏天闪亮登场,而这一次亮相,为整个 Swift 生态环境带来了巨大的变化。

或者,正如 Nataliya Patsovska 在 一篇推特 中所提到的:

iOS API 设计简史:

  • Objective C - 在名字中描述了所有语义,类型并不重要
  • Swift 1 到 5 - 名字侧重于清晰度,基础结构体,枚举,类和协议持有语义
  • Swift 5.1 - @wrapped \$path @yolo

——@nataliya_bg

也许我们后面回头看才能知道, Swift 5.1 是不是为我们热爱的语言树立了一个临界点或者转折点。

本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 http://swift.gg

Swift 中类型别名的用途

作者:Benedikt Terhechte,原文链接,原文日期:2019-05-15
译者:Ji4n1ng;校对:WAMakerNemocdz;定稿:Pancf

什么是 typealias

当我们回忆那些 Swift 强大的语言特性时,很少有人会首先想到 typealias。然而,许多情况下类型别名会很有用。本文将简要介绍 typealias 是什么,如何定义它,并列出多个示例说明如何在自己的代码中使用它们。让我们开始深入了解吧!

顾名思义,typealias 是特定类型的别名。类型,例如 IntDoubleUIViewController 或一种自定义类型。Int32Int8 是不同的类型。换句话说,类型别名在你的代码库里插入现有类型的另一个名称。例如:

typealias Money = Int

Int 类型创建别名。这样就可以在代码中的任何地方使用 Money,就像是 Int 一样:

struct Bank {
typealias Money = Int
private var credit: Money = 0
mutating func deposit(amount: Money) {
credit += amount
}
mutating func withdraw(amount: Money) {
credit -= amount
}
}

上面有一个结构体 Bank 来管理钱。但是,没有使用 Int 作为金额,而是使用 Money 类型。可以看出 +=-= 运算符仍然可以按预期工作。

还可以混合使用类型别名和原始类型,以及匹配二者。可以这么做是因为对于 Swift 编译器来说,它们都解析为同一个东西:

struct Bank {
typealias DepositMoney = Int
typealias WithdrawMoney = Int
private var credit: Int = 0
mutating func deposit(amount: DepositMoney) {
credit += amount
}
mutating func withdraw(amount: WithdrawMoney) {
credit -= amount
}
}

在这里,我们混合使用了 Int 及其不同自定义类型别名 DepositMoneyWithdrawMoney

泛型类型别名

除上述内容外,类型别名也可以具有泛型参数:

typealias MyArray<T> = Array<T>
let newArray: MyArray = MyArray(arrayLiteral: 1, 2, 3)

上面,为 MyArray 定义了一个类型别名,该别名与常规数组一样。最后,类型别名的泛型参数甚至可以具有约束。想象一下,我们希望新的 MyArray 只保留遵循 StringProtocol 的类型:

typealias MyArray<T> = Array<T> where T: StringProtocol

这是一个不错的特性,你可以快速为特定类型定义数组,而不必将 Array 子类化。说到这里,让我们看一下类型别名的一些实践应用。

实践应用

更清晰的代码

第一个,同时也显而易见的用例,我们已经简要介绍过了。类型别名可以使代码更具含义。在 typealias Money = Int 示例中,我们引入了 Money 类型——一个清晰的概念。像 let amount: Money = 0 这样来使用它,比 let amount: Int = 0 更容易理解。在第一个示例中,你立刻就明白这是金钱数额。而在第二个示例中,它可以是任何东西:自行车的数量、字符的数量、甜甜圈的数量——这谁知道!

这显然不是都必要的。如果函数签名已经清楚地说明了参数的类型(func orderDonuts(amount: Int)),那么包含其他的类型别名将是不必要的开销。另一方面,对于变量和常量来说,它通常可以提高可读性并极大地帮助编写文档。

更简单的可选闭包

Swift 中的可选闭包有点笨拙。接受一个 Int 参数并返回 Int 的闭包的常规定义如下所示:

func handle(action: (Int) -> Int) { ... }

现在,如果要使此闭包为可选型,则不能仅添加问号:

func handle(action: (Int) -> Int?) { ... }

毕竟,这不是一个可选型的闭包,而是一个返回可选 Int 的闭包。正确的方法是添加括号:

func handle(action: ((Int) -> Int)?) { ... }

如果有多个这样的闭包,这将变得尤为难看。下面,有一个函数,它可以处理成功和失败情况,以及随着操作的进行调用一个附加的闭包。

func handle(success: ((Int) -> Int)?,
failure: ((Error) -> Void)?,
progress: ((Double) -> Void)?) {

}

这小段代码包含很多括号。由于我们不打算成为 lisper(译者注:lisp 语言使用者),因此想通过对不同的闭包使用类型别名来解决此问题:

typealias Success = (Int) -> Int
typealias Failure = (Error) -> Void
typealias Progress = (Double) -> Void

func handle2(success: Success?, failure: Failure?, progress: Progress?) { ... }

实际上,这个函数看起来确实更具可读性。虽然这很好,但我们确实通过使用三行 typealias 引入了其他语法。但是,从长远来看,这实际上可能对我们有帮助,就像我们将在接下来看到的。

集中定义

这些特定类型不仅仅可以用在前面示例的那些操作处理器中。下面是经过略微修改,更符合实际使用的操作处理器类:

final class Dispatcher {
private var successHandler: ((Int) -> Void)?
private var errorHandler: ((Error) -> Void)?

func handle(success: ((Int) -> Void)?, error: ((Error) -> Void)?) {
self.successHandler = success
self.errorHandler = error
internalHandle()
}

func handle(success: ((Int) -> Void)?) {
self.successHandler = success
internalHandle()
}

func handle(error: ((Int)-> Void?)) {
self.errorHandler = error
internalHandle()
}

private func internalHandle() {
...
}
}

该结构体引入了两个闭包,一个用于成功情况,一个用于错误情况。但是,我们还希望提供更方便的函数,调用其中一个处理器即可。在上面的示例中,如果要向成功和错误处理器添加另一个参数(例如 HTTPResponse),那么需要更改很多代码。在三个地方,((Int) -> Void)? 需要变成 ((Int, HTTPResponse) -> Void)?。错误处理器也是一样的。通过使用多个类型别名,可以避免这种情况,只需要在一个地方修改类型:

final class Dispatcher {
typealias Success = (Int, HTTPResponse) -> Void
typealias Failure = (Error, HTTPResponse) -> Void

private var successHandler: Success?
private var errorHandler: Failure?

func handle(success: Success?, error: Failure?) {
self.successHandler = success
self.errorHandler = error
internalHandle()
}

func handle(success: Success?) {
self.successHandler = success
internalHandle()
}

func handle(error: Failure?) {
self.errorHandler = error
internalHandle()
}

private func internalHandle() {
...
}
}

这不仅易于阅读,而且随着在更多地方使用该类型,它也会继续发挥它的作用。

泛型别名

类型别名也可以是泛型的。一个简单的用例是强制执行具有特殊含义的容器。假设我们有一个处理图书的应用。一本书由章节组成,章节由页面组成。从根本上讲,这些只是数组。下面是 typealias

struct Page {}
typealias Chapter = Array<Page>
typealias Book = Array<Chapter>

与仅使用数组相比,这有两个好处。

  1. 该代码更具解释性。
  2. 包装页面的数组能包含页面,而不能包含其它的。

回顾我们先前使用成功失败处理程序的示例,我们可以通过使用泛型处理程序来进一步改进:

typealias Handler<In> = (In, HTTPResponse?, Context) -> Void

func handle(success: Handler<Int>?,
failure: Handler<Error>?,
progress: Handler<Double>?,)

这样的组合确实非常棒。这使我们能够编写一个更简单的函数,并可以在一个地方编辑 Handler

这种方法对于自定义的类型也非常有用。你可以创建一个泛型定义,然后定义详细的类型别名:

struct ComputationResult<T> {
private var result: T
}

typealias DataResult = ComputationResult<Data>
typealias StringResult = ComputationResult<String>
typealias IntResult = ComputationResult<Int>

再说一遍,类型别名允许我们编写更少的代码并简化代码中的定义。

像函数一样的元组

同样,可以使用泛型和元组来定义类型,而不是必须用结构体。下面,我们设想了一种遗传算法的数据类型,它可以在多代中修改其值 T

typealias Generation<T: Numeric> = (initial: T, seed: T, count: Int, current: T)

如果定义这样的类型别名,则实际上可以像初始化一个结构体那样对其进行初始化:

let firstGeneration = Generation(initial: 10, seed: 42, count: 0, current: 10)

尽管它看起来确实像一个结构体,但它只是一个元组的类型别名。

组合协议

有时,你会遇到一种情况,你有多个协议,而且需要使用一个特定类型来把这些协议都实现。这种情况通常发生在当你定义了一个协议层来提高灵活性时。

protocol CanRead {}
protocol CanWrite {}
protocol CanAuthorize {}
protocol CanCreateUser {}

typealias Administrator = CanRead & CanWrite & CanAuthorize & CanCreateUser

typealias User = CanRead & CanWrite

typealias Consumer = CanRead

在这里,我们定义了权限层。管理员可以做所有事情,用户可以读写,而消费者只能读。

关联类型

这超出了本文的范围,但是协议的关联类型也可以通过类型别名来定义:

protocol Example {
associatedtype Payload: Numeric
}

struct Implementation: Example {
typealias Payload = Int
}

缺点

尽管类型别名是一个非常有用的功能,但它们有一个小缺点:如果你不熟悉代码库,那么对下面这两个定义的理解会有很大区别。

func first(action: (Int, Error?) -> Void) {}
func second(action: Success) {}

第二个不是立即就能明白的。Success 是什么类型?如何构造它?你必须在 Xcode 中按住 Option 单击它,以了解它的功能和工作方式。这会带来额外的工作量。如果使用了许多类型别名,则将花费更多的时间。这没有很好的解决方案,(通常)只能依赖于用例。

最后

我希望你能喜欢这篇关于类型别名可能性的小总结。如果你有任何反馈意见,可以在 Twitter 上找到我

本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 http://swift.gg

NSPredicate

作者:Jordan Morgan,原文链接,原文日期:2018-05-18
译者:石榴;校对:numbbbbbNemocdz;定稿:Pancf

Swift 刚出现的时候,我们因它比 Objective-C 简洁而着迷。接着它很快打开了面向协议编程的大门。并且,让我们忘掉引用类型和类,还有很多。

确实,这些东西都是很棒的工具,都有优秀的用例。但我感觉它们经常被捧作银弹,在决定架构时缺乏足够的考虑。

因此在 2018 年,技术博客中充斥着各种 Swift 黑魔法(我的博客也不例外🤷🏻‍♂️),会议演讲也都在讨论 Swift 的函数式编程未来(没错,我也做了这种演讲🙋🏻‍♂️)。

所有人都对在 Swift 中使用集合感到激动,但是我们从 iOS 3 开始就可以用 Objective-C 来做相似的事了。所以今天我会讨论 NSPredicate 的威力,以及如何用🦖筛选集合。

有必要提一下:我们最近看到了一些开发者一开始学了 Swift,后来又得回去维护 Objective-C 的代码。如果说的就是你,那你很可能正在发愁如何优雅地在 Objective-C 中处理集合。

这里讲的东西可能对你有用。

用例

近几年来,Objective-C 的集合有了长足的进步。还在几年以前,我们还必须教这愚蠢的编译器:

NSString *aString = (NSString *)[anArray indexOfObject:0];

感谢老天、库比提诺[^1]和朋友们©终于用类型擦除的方式添加了泛型。这是一个很大的进步:

[^1]: 译者注:Cupertino, CA,苹果总部所在城市。

NSArray *anArray = @[@"Sup"];
NSString *aString = [anArray firstObject];

但无论是不是泛型,我们经常通过与下面类似的方法与 Objective-C 集合中的内容交互:

for (NSString *str in anArray)
{
if ([str isEqualToString:@"The Key"])
{
// 做些什么
}
}

很多情况下,这样写是可以接受的。但是当需求越来越复杂,关系更加多种多样,代码就会变得不确定。如果你遵从代码更少更稳定更容易维护的观念,那么这种简单的查询集合操作也可能成为困扰。

Predicate 可以改善这个状况。不是要在代码中耍些小聪明,而是写出简洁和实用的代码。

概览

NSPredicate 的核心用途是限制或定义对内存中的数据过滤,或进行取回时的参数。当它和 Core Data 一起使用的时候才会如虎添翼。它和 SQL 很像,只不过没那么糟糕*。

开个玩笑,只是我对基于集合的操作都无感🙃。

你给它提供逻辑条件,然后就会返回符合条件的东西。这意味着它可以提供基础比较、复合 predicate、KeyPath 集合查询、子查询、合计以及更多的支持。

因为它用来筛选集合,它可以获得 Foundation 类的原生支持。使用可变版本时支持用结果直接修改,不可变版本会返回一个新实例:

// 修改原数组
[mutableArray filterUsingPredicate:/*NSPredicate*/];

// 返回新的数组
[mutableArray filteredArrayUsingPredicate:/*NSPredicate*/];

虽然 predicate 可以从 NSExpressionNSCompoundPredicateNSComparsionPredicate 中实例化,但它还可以用一个字符串的语法生成。这和可视化格式语言类似,我们可以用它定义排版约束。

在这里我们主要关注能用字符串语法生成的能力。

配置

为了更好的说明,文章的剩余部分以下面的代码为前提。

// 伪代码
Person:NSObject
Identifier:NSString
Name:NSString
PayGrade:NSNumber

// 某个含有 Person 实例的属性
NSArray *employees

查询⚡️

本文剩下都在用直接的例子来介绍如何用字符串格式语法来配置查询。

我们可以从一个简单的搜索的情景开始。先假设我们有一个含有表示 Person 对象标识符的数组:

{
@"erersdg32453tr",
@"dfs8rw093jrkls",
// etc
}

现在,我们想通过这些识别符从一个现存的 Person 数组中获取 Person 对象。可以使用一个双层嵌套的 for 循环来解决这个问题:

// 假设 "employees" 是一个存有 Person 对象的数组
NSArray *morningEventAttendees = @[/*上面的人的识别符*/];
NSMutableArray *peopleAttendingMorningEvent = [NSMutableArray new];

for (NSString *userID in morningEventAttendees)
{
for (Person *person in employees)
{
if ([person.identifier isEqualToString:userID])
{
[peopleAttending addObject:person];
}
}
}

// 现在 peopleAttendingMorningEvent 里面就有我们想要的东西了

我们也可以使用 predicate 来达到完全一样的效果:

NSPredicate *morningAttendees = [NSPredicate predicateWithFormat:@"SELF.identifier IN %@", peopleAttendingMorningEvent];

NSArray *peopleAttendingMorningEvent = [employees filteredArrayUsingPredicate:morningAttendees];

💫。

Predicate 的语法允许我们使用 SELF,它在这里发挥了很大的作用。它表示数组里正在被操作的对象,在这里就是 Person 对象。

另一个额外的好处是我们不用把数组定义成可变的了。

正是因为这个原因,我们可以访问与 SELF 所表示对象关联的 KeyPath。在上面的代码中,引用了 identifier 属性。

如果你喜欢的话,任何 KeyPath 可以用放在 “%K” 位置的变量来表示。这个版本和上面的效果一样:

[NSPredicate predicateWithFormat:@"SELF.%K IN %@", @"identifier", peopleAttendingMorningEvent];

复合 Predicate

合并多个比较很简单。假设我们还需要像上面一样找到所有参加活动的人,但还要满足他们的工资水平在 50000 到 60000 之间。

如果使用传统的方法,我们的 if 语句只会越写越长:

// 和上面的代码一样
if ([person.identifier isEqualToString:userID] && (person.paygrade.integerValue >= 5 && person.paygrade.integerValue <= 10))
{
[peopleAttending addObject:person];
}

但使用一个重构过的 predicate 可以让我们用一种更符合语言习惯的方式来解决问题:

NSPredicate *morningAttendees = [NSPredicate predicateWithFormat:@"SELF.identifier IN %@ && SELF.paygrade.integerValue BETWEEN {50000, 60000}", peopleAttendingMorningEvent];

它允许用不同的操作符表示同样的作用,可以根据你的偏好来提升可读性。比如:

  • “&&” 或 “AND”
  • “||” 或 “OR”
  • “!” 或 “NOT”

如你所想,它们经常会在基本比较操作之间出现,聚合在一个 predicate 里。

字符串比较

我们经常会处理一些基于字符串比较的匹配。大家都知道 Objective-C 对冗余代码的无止尽追求,在处理 NSString 的时候也丝毫不减:

NSString *name = @"Jordan";
name = [name stringByAppendingString:[NSString stringWithFormat:@"%@ %@", @"Wesley", @"Morgan"]];

……而 Swift 则一边偷笑,一边低调地把字符串们连接起来。幸亏我们在用 NSPredicate 来比较字符串时不会写出上面那么冗余的代码。

// 假设 mutablePersonAr 是一个 Person 数组,里面有 "Karl" 和 "Jordan"
NSPredicate *namesStartingWithK = [NSPredicate predicateWithFormat:@"SELF.name BEGINSWITH 'K'"];
// 现在只有 Karl 了
[mutablePersonAr filterUsingPredicate:namesStartingWithK];

实际上任何比较都可以用 predicate 语法中的 CONTAINSBEGINSWITHENDSWITHLIKE 来实现:

// 假设 mutablePersonAr 是一个 Person 数组,里面有 "Karl" 和 "Kathryn"
NSPredicate *namesStartingWithK = [NSPredicate predicateWithFormat:@"SELF.name LIKE 'Kar*'"];

// 现在只有 Karl 了
[mutablePersonAr filterUsingPredicate:namesStartingWithK];

你可能已经注意到上面的星号了;和很多的 DSL 一样,这个星号代表一个通配符。

当你在一个查询里结合多个比较运算符时,这种简洁用法的重要性就会体现出来了:

NSString *predicateFormat = @"(SELF.name LIKE 'Kar*') AND (SELF.paygrade.intValue >= 10)";

NSPredicate *namesStringWithK = [NSPredicate predicateWithFormat:predicateFormat];

// 现在只有 Karl 了
[mutablePersonAr filterUsingPredicate:namesStartingWithK];

更进一步,它还支持用 MATCHES 语法实现 NSPredicate 的类 SQL 语法与正则表达式混用:

[NSPredicate predicateWithFormat:@"SELF.phoneNumber MATCHES %@", phoneNumberRegex];

然而是时候该指出一点,predicate 语法十分严格。它就是一个字符串。除非你是 Mavis Beacon[^2], 否则你总会一遍又一遍地不小心打错字。

[^2]: 译者注:Mavis Beacon Teaches Typing,一款在 1987 年发售的教盲打的软件。

好消息是你会很快的发现问题 — 运行时的异常在等着你。我们获得了能力和灵活性,但在某种程度上失去了静态检查的安全性。

为了说明这一点,这段从上面代码稍微修改而来的代码会导致崩溃。你能看出来是为什么吗?

NSString *predicateFormat = @"SELF.name LIKE 'Kar*') AND (SELF.paygrade.intValue >= 10)"

NSPredicate *namesStartingWithK = [NSPredicate predicateWithFormat:predicateFormat];

// 现在只有 Karl 了
[mutablePersonAr filterUsingPredicate:namesStartingWithK];

为了减轻这些问题,我经常把 predicate 和 NSStringFromSelector() 结合在一起用,以此应对错别字和为以后的重构提供多一层安全保障。

NSString *predicateFormat = @"(SELF.%@ LIKE 'Kar*') AND (SELF.paygrade.intValue >= 10)"

NSString *kpName = NSStringFromSelector(@selector(identifier));
NSString *kpPaygrade = NSStringFromSelector(@selector(paygrade));

NSPredicate *namesStartingWithK = [NSPredicate predicateWithFormat:predicateFormat, kpName, kpPaygrade];

// 现在只有 Karl 了
[mutablePersonAr filterUsingPredicate:namesStartingWithK];

有点复杂了?确实。更安全了?毫无疑问。

KeyPath 集合查询

由于基于 KeyPath 的用法,NSPredicate 拥有一全套工具去操作它们,以提供一个更好的搜索。考虑下面的代码:

// 假设一个 Person 对象现在有一个下面的属性:
// NSArray *previousPay

// 找到所有满足过去工资的平均值大于 10 的人
NSString *predicateFormat = @"SELF.previousPay.@avg.doubleValue > 10";
NSPredicate *previousPayOverTen = [NSPredicate predicateWithFormat:predicateFormat];

// 所有过去工资的平均值大于 10 的人
[mutablePersonAr filterUsingPredicate:previousPayOverTen];

你可以把 @avg 换成:

  • @sum
  • @max
  • @min
  • @count

想象下如果不使用 predicate 情况下完成同样的工作,就不得不写大量尽管很简单的代码。你可以开始将这些技巧用在你日常的工具链里。

对数组的深究

和 KeyPath 查询很像,predicate 也支持以更细的维度检查数组:

  • array[FIRST]
  • array[LAST]
  • array[SIZE]
  • array[index]

应用在上面的代码样例上,我们就可以这样查询:

// 找到所有过去有三份不同工资的人
NSString *predicateFormat = @"previousPay[SIZE] == 3";

NSPredicate *threePreviousSalaries = [NSPredicate predicateWithFormat:predicateFormat];

// 这些 Person 对象过去有三份不同的工资
[mutablePersonAr filterUsingPredicate:threePreviousSalaries];

和在上面提到的一样,我们也可以应用多个条件:

// 找到所有过去有三个不同的工资以及第一份工资大于 8 的人
NSString *predicateFormat = @"(previousPay[SIZE] == 3) AND (previousPay[FIRST].intValue > 8)";

NSPredicate *predicate = [NSPredicate predicateWithFormat:predicateFormat];
[mutablePersonAr filterUsingPredicate:predicate];

更加深入,你可以使用下面的操作符来实现更复杂的操作:

  • @distinctUnionOfArrays
  • @unionOfArrays
  • @unionOfObjects
  • @distinctUnionOfObjects

假设我们有一个含有 Person 对象的数组,我们需要的是找出在所有数组中识别符不同的 Person 实例:

// 假设 p1/2/3/4 都是 Person 对象
NSArray *> *previousEmployees = @[@[p1],@[p2,p1,p2],@[p1],@[p4,p2],@[p4],@[p4],@[p1]];

// 获取所有不同的 ID
NSArray *unqiuePreviousEmployeeIDs = [previousEmployees valueForKeyPath:@"@distinctUnionOfObjects.identifier"];

// 现在数组里应该只含有不同的 ID

厉害吧!

还有更好玩的呢,还支持子查询:

// 假设 Person 对象有了一个新的属性表示他们的队伍:
// NSArray *team;

// 从雇员数组中找出这样的人,他们的团队中有人满足这个条件:没有历史工资数据并且工资大于 1
NSString *predicateFormat = @"SUBQUERY(team, $teamMember, $teamMember.paygrade.intValue > 1 AND $teamMember.previousPay == nil).@count > 0";

NSPredicate *predicate = [NSPredicate predicateWithFormat:predicateFormat];
[employeeAr filterUsingPredicate:predicate];

当你发现你需要在一个含有对象的数组里搜索,而这些对象含有的属性本身就是一个集合的时候,子查询十分有用。所以在上面的例子里,我们有一个 Person 对象的数组,并且查询它的 teamMember 数组。

便捷才是关键[^3]

[^3]: 译者注:此处原作者用了双关。原文是 “Convenience is Key(Path)”,既有便捷是关键的意思,又在暗指这里的关键其实是 Key Path。

尽管 NSPredicate 是为了搜索而设计出来的,但如果你不把它用在和原本设计稍微偏离的地方那它就不是 Objective-C 了。这里也不例外。

当你想到 predicate,你想到的是从一个集合里筛选 — 也就是说它的返回值(或更改过的原来数组)还含有相同的东西。

但是也可以让他们含有同的东西。其实我们在之前的代码中已经这样操作过了。上面的二维数组被用来返回一个识别符的数组 — NSString 实例。KeyPath 让这些变得可能。

这有一个更直接的例子:

// 我们得到一个长度大于 10 的识别符字符串的数组
NSString *predicateFormat = @"SELF.identifier.length > 10";
NSPredicate *predicate = [NSPredicate predicateWithFormat:predicateFormat];
NSArray *longEmployeeIDs = [[employeeArray filteredArrayUsingPredicate:predicate] valueForKey:@"identifier"];

// 现在 longEmployeeIDs 已经不含有 Person 对象了,只有字符串

总结

马上在 Objective-C 的集合里使用这些语法糖,这样就可以不使用嵌套循环从一个特定的子集中提取数据。使用 NSPredicate 可以让眼睛轻松很多。

虽然 Swift 从语言级别支持对集合进行切片操作,但使用创建的 NSPredicate 对象来解决相同的问题也不难。如果你发现你在维护一个成熟的代码库,或是需要用上古时代 Objective-C 的新项目,随心所欲的使用 predicate 吧。

下次见吧✌️。

本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 http://swift.gg

调试内存不足问题:使用运行时魔法捕获布局反馈循环

作者:Ruslan Serebriakov,原文链接,原文日期:2019-01-10
译者:Sunset Wan;校对:numbbbbbNemocdz;定稿:Pancf

让我们想象这样一个场景:你有一个很成功的应用程序,这个应用程序有大量的日活用户并且崩溃率为 0。你很开心并且认为生活很美好。但是某天开始,你在应用商店上看到关于应用程序总是崩溃的负面评价。检查 Fabric 也没有起到任何帮助————没有出现新的崩溃记录。那么,这个现象可能是什么原因呢?

答案是内存不足(Out Of Memory),从而导致应用程序运行终止。

当你在终端用户的设备上使用 RAM 时,操作系统可以决定是否为其他进程而回收该内存,并终止你的应用程序。我们把它称作内存不足异常终止。
可能有各种原因:

  • 循环引用;
  • 竞争条件;
  • 废弃的线程;
  • 死锁;
  • 布局反馈循环。

Apple 提供了很多方法来解决这类问题:

  • Instruments 里的 Allocations 和 Leaks 工具用于解决循环引用和 其他类型的泄漏
  • 在 Xcode 8 中引入的 Memory Debugger 代替了 Allocations 和 Leaks 的一部分功能
  • Thread Sanitizer 帮助你找到竞争条件、废弃的线程或者死锁

布局反馈循环

下面我们来讨论下布局反馈循环。它不是一个频繁出现的问题,一旦遇到了,可能让你很头痛。

当视图正在运行它们的布局代码,但某种方法导致它们再一次开始布局传递,此时布局反馈循环就会出现。这可能是因为某个视图正在改变某个父视图的大小,或者因为你有一个模棱两可的布局。无论哪种原因,这个问题的表现是你的 CPU 使用被占满和 RAM 使用量稳步上升,因为你的视图正在一次又一次地运行它们的布局代码,却没有返回。
- 来自 HackingWithSwift 的 Paul Hudson

幸运的是,在 WWDC 16 中 Apple 花了整整 15 分钟(!)来介绍“布局反馈循环调试器”。这个调试器有助于识别在调试过程中发生循环的时间点。这就是一个符号断点,它的工作方式非常简单:它会计算在单个 run loop 迭代中调用每个视图上的 layoutSubviews() 方法的次数。一旦这个计数值超过某个临界值(比如,100),这个应用程序将会停在这个断点并打印出日志,来帮助你找到根本原因。这篇文章 快速地介绍如何使用这个调试器。

这个方法在你可以重现问题的情况下十分有效。但是如果你有几十个屏幕,几百个视图,但应用商店中你的应用程序的评价仅仅是:“这个应用程序糟透了,总是崩溃,再也不用了!”。你希望可以将这些人带到办公室并为他们设置布局反馈循环调试器。虽然因为通用数据保护条例(GDPR),这部分无法完全实现,但是你可以尝试把 UIViewLayoutFeedbackLoopDebuggingThreshold 的代码复制到生产代码中。

让我们回顾一下符号断点是如何工作的:它会计算 layoutSubviews() 的调用次数并在单个 run loop 迭代中超过某个临界值时发送一个事件。听起来很简单,对吧?

class TrackableView: UIView {
var counter: Int = 0

override func layoutSubviews() {
super.layoutSubviews()

counter += 1;
if (counter == 100) {
YourAnalyticsFramework.event(name: "loop")
}
}
}

对于一个视图,这段代码运行正常。但是现在你想要在另一个视图上实现它。当然,你可以创建一个 UIView 的子类,在这里实现它并使你项目中的所有视图都继承这个子类。然后为 UITableViewUIScrollViewUIStackView 等做同样的事情。

你希望可以将此逻辑注入你想要的任何类,而无需编写大量重复的代码。这时候就可以借助运行时编程了。

我们会做同样的事情——创建一个子类,重写 layoutSubviews() 方法并计算其调用次数。唯一的区别是所有这些都将在运行时完成,而不是在项目中创建重复的类。

让我们开始吧——我们将创建自定义子类,并将原始视图的类更改为新的子类:

struct LayoutLoopHunter {

struct RuntimeConstants {
static let Prefix = “runtime”
}

static func setUp(for view: UIView, threshold: Int = 100, onLoop: @escaping () -> ()) {
// 我们根据功能的前缀和原始类名为新类创建名称。
let classFullName = “\(RuntimeConstants.Prefix)_\(String(describing: view.self))”
let originalClass = type(of: view)

if let trackableClass = objc_allocateClassPair(originalClass, classFullName, 0) {
// 在当前运行时会话期间尚未创建此类。
// 注册这个类,并且用原始视图的类来和它交换。
objc_registerClassPair(trackableClass)
object_setClass(view, trackableClass)
} else if let trackableClass = NSClassFromString(classFullName) {
// 我们之前在此运行时会话中分配了一个具有相同名称的类。
// 我们可以从原始字符串中获取它,并以相同的方式与我们的视图交换。
object_setClass(view, trackableClass)
}
}
}

objc_allocateClassPair() 方法的文档告诉我们这个方法何时失败:

新类,或者如果无法创建类,则为 Nil (例如,所需名称已被使用)。

这就意味着不能拥有两个同名的类。我们的策略是为单个视图类创建一个单独的运行时类。这就是我们在原始类名前加上前缀来形成新类的名称的原因。

现在添加一个计数器到子类中。理论上,有两种方法可以做到这一点。

  1. 添加一个保存计数器的属性。
  2. 为这个类创建一个关联对象(Associated object)。

但是目前,只有一个方法奏效。你可以想象属性是存储在分配给类的内存里的东西,然而关联对象则储存在一个完全不同的地方。因为分配给已存在对象的内存是固定的,所以我们在自定义类上新添加的属性将会从其他资源里“窃取”内存。它可能导致意料之外的行为和难以调试的程序崩溃(点击 这里 查看更多信息)。但是在使用关联对象的情况下,它们将会存储在运行时创建的一个哈希表里,这是完全安全的。

static var CounterKey = "_counter"

...

objc_setAssociatedObject(trackableClass, &RuntimeConstants.CounterKey, 0, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)

当新的子类被创建时,计数器初值设置为 0。接下来,让我们实现这个新的layoutSubviews() 方法,并将它添加到我们的类中:

let layoutSubviews: @convention(block) (Any?) -> () = { nullableSelf in
guard let _self = nullableSelf else { return }

if let counter = objc_getAssociatedObject(_self, &RuntimeConstants.CounterKey) as? Int {
if counter == threshold {
onLoop()
}

objc_setAssociatedObject(trackableClass, &RuntimeConstants.CounterKey, counter+1, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
let implementation = imp_implementationWithBlock(layoutSubviews)
class_addMethod(trackableClass, #selector(originalClass.layoutSubviews), implementation, "v@:")

为了理解上面这段代码实际上在干什么,让我们看一下这个来自 <objc/runtime.h> 的结构体:

struct objc_method {
SEL method_name;
char *method_types;
IMP method_imp;
}

尽管我们再也不会在 Swift 中直接使用这个结构体,但它很清楚地解释了一个方法实际上是由什么组成的:

  • 方法的实现,这是调用方法时要执行的实际函数。它的前两个形参总是方法接收者和消息选择器。
  • 包含方法签名的方法类型字符串。你可以在 这里 详细了解其格式。但是在现在的情况下,需要明确说明的字符串是 “v@:”。作为返回类型,v 代表 void,而 @: 分别代表接收者和消息选择器。
  • 选择器作为键,用于在运行时查找方法的实现。

你可以把 Witness Table(在其他编程语言中,它也被称作方法派发表)想象成一个简单的字典数据结构。那么选择器为键,且实现部分则为对应的值。
在下面这行代码中:

class_addMethod(trackableClass,#selector(originalClass.layoutSubviews), implementation, "v@:")

我们所做的是给 layoutSubviews() 方法对应的键分配新值。

这个方法直截了当。我们获得这个计数器,使它的计数值加一。如果计数值超过临界值,我们会发送分析事件,其中包含类名和想要的任何数据体。

让我们回顾一下如何对关联对象实现和使用键:

static var CounterKey = “_counter”
...

objc_setAssociatedObject(trackableClass, &RuntimeConstants.CounterKey, counter+1, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)

为什么我们使用 var 来修饰计数器的键这个静态属性并在传递到其他地方时使用引用?答案隐藏在 Swift 语言基础——字符串之中。字符串像其他所有的值类型一样,是按值传递的。那么,当你把它传入这个闭包时,这个字符串将会被复制到一个不同的地址,这会导致在关联对象表中产生一个完全不同的键。& 符号总是保证将相同的地址作为键参数的值。你可以尝试以下代码:

func printAddress(_ string: UnsafeRawPointer) {
print("\(string)")
}

let str = "test"

printAddress(str)
printAddress(str)
let closure = {
printAddress(str)
printAddress(str)
}
closure()
// 最后两个函数调用的地址将始终不同

用引用的方式来传递键的主意总是好的,因为有时,即使你没有使用闭包,变量的地址仍可能因内存管理而更改。在我们例子中,如果你把上面的代码运行多次,即使是前两个 printAddress() 的调用也可能会输出不同的地址。

让我们回到运行时的魔法里来。在新 layoutSubviews() 的实现里,还有一件很重要的事情没有完成。这件事是每次重写父类的方法时通常都会做的事情——调用父类实现。layoutSubviews() 的文档里提到:

在 iOS 5.1 及更早版本中,这个方法的默认实现不执行任何操作。而之后的默认实现会使用你设置的任何约束来确定任何子视图的大小和位置。

为了避免发生一些难以预料的布局行为,我们得调用父类的实现,但这不像平常那样简单明了:

let selector = #selector(originalClass.layoutSubviews)
let originalImpl = class_getMethodImplementation(originalClass, selector)


// @convention(c) 告知 Swift 这是一个裸函数指针(没有上下文对象)
// 所有的 Obj-C 方法函数把接收者和消息当作前两个参数
// 所以这意味着一个类型为 `() -> Void` 的方法,这与 `layoutSubview` 方法相符
typealias ObjCVoidVoidFn = @convention(c) (Any, Selector) -> Void
let originalLayoutSubviews = unsafeBitCast(originalImpl, to: ObjCVoidVoidFn.self)
originalLayoutSubviews(view, selector)

这里实际发生的是:我们检索方法所需的实现部分,并直接从代码中调用它,而不是用常见的方式来调用方法(即执行一个会在 Witness Table 中寻找对应实现的选择器)。

目前为止,让我们看看实现部分:

static func setUp(for view: UIView, threshold: Int = 100, onLoop: @escaping () -> ()) {
// 我们根据功能的前缀和原始类名为新类创建名称
let classFullName = “\(RuntimeConstants.Prefix)_\(String(describing: view.self))”
let originalClass = type(of: view)

if let trackableClass = objc_allocateClassPair(originalClass, classFullName, 0) {
// 在当前运行时会话期间尚未创建此类
// 注册这个类并将其与原始视图的类交换
objc_registerClassPair(trackableClass)
object_setClass(view, trackableClass)

// 现在可以创建关联对象
objc_setAssociatedObject(view, &RuntimeConstants.CounterKey, 0, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)

// 添加我们自己 layoutSubviews 的实现
let layoutSubviews: @convention(block) (Any?) -> () = { nullableSelf in
guard let _self = nullableSelf else { return }

let selector = #selector(originalClass.layoutSubviews)
let originalImpl = class_getMethodImplementation(originalClass, selector)


// @convention(c) 告知 Swift 这是一个裸函数指针(没有上下文对象)
// 所有的 Obj-C 方法函数把接收者和消息当作前两个参数
// 所以这意味着一个类型为 `() -> Void` 的方法,这与 `layoutSubview` 方法相符
typealias ObjCVoidVoidFn = @convention(c) (Any, Selector) -> Void
let originalLayoutSubviews = unsafeBitCast(originalImpl, to: ObjCVoidVoidFn.self)
originalLayoutSubviews(view, selector)

if let counter = objc_getAssociatedObject(_self, &RuntimeConstants.CounterKey) as? Int {
if counter == threshold {
onLoop()
}

objc_setAssociatedObject(view, &RuntimeConstants.CounterKey, counter+1, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
let implementation = imp_implementationWithBlock(layoutSubviews)
class_addMethod(trackableClass, #selector(originalClass.layoutSubviews), implementation, “v@:“)
} else if let trackableClass = NSClassFromString(classFullName) {
// 我们之前在此运行时会话中分配了一个具有相同名称的类
// 我们可以从原始字符串中获取它,并以相同的方式与我们的视图交换
object_setClass(view, trackableClass)
}
}

让我们为视图创建模拟布局循环,并为其设置计数器来进行测试:

class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()

LayoutLoopHunter.setUp(for: view) {
print("Hello, world")
}
}

override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
view.setNeedsLayout() // loop creation
}
}

是不是忘记了什么事情?让我们再次回顾一下 UIViewLayoutFeedbackLoopDebuggingThreshold 断点的工作原理:

在确认为反馈循环之前,定义某个视图的子视图在单个 run loop 里必须布局的次数

我们从未把“单个 run loop ”这一条件考虑进来。如果视图在屏幕上停留了相当长的时间,并经常被反复布局,计数器迟早会超过临界值。但这可不是因为内存的问题。

我们该怎么解决这个问题呢?只需在每次 run loop 迭代时重置计数器。为了做到这一点,我们可以创建一个 DispatchWorkItem,它重置计数器,并在主队列上异步传递它。通过这种方式,它会在 run loop 下一次进入主线程时被调用:

static var ResetWorkItemKey = “_resetWorkItem”

...

if let previousResetWorkItem = objc_getAssociatedObject(view, &RuntimeConstants.ResetWorkItemKey) as? DispatchWorkItem {
previousResetWorkItem.cancel()
}
let currentResetWorkItem = DispatchWorkItem { [weak view] in
guard let strongView = view else { return }
objc_setAssociatedObject(strongView, &RuntimeConstants.CounterKey, 0, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
DispatchQueue.main.async(execute: currentResetWorkItem)
objc_setAssociatedObject(view, &RuntimeConstants.ResetWorkItemKey, currentResetWorkItem, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)

最终的代码:

struct LayoutLoopHunter {

struct RuntimeConstants {
static let Prefix = “runtime”

// Associated objects keys
// 关联对象键
static var CounterKey = “_counter”
static var ResetWorkItemKey = “_resetWorkItem”
}

static func setUp(for view: UIView, threshold: Int = 100, onLoop: @escaping () -> ()) {
// 我们根据功能的前缀和原始类名为新类创建名称。
let classFullName = “\(RuntimeConstants.Prefix)_\(String(describing: view.self))”
let originalClass = type(of: view)

if let trackableClass = objc_allocateClassPair(originalClass, classFullName, 0) {
// 在当前运行时会话期间尚未创建此类。
// 注册这个类,并且用原始视图的类来和它交换。
objc_registerClassPair(trackableClass)
object_setClass(view, trackableClass)

// 现在可以创建关联对象
objc_setAssociatedObject(view, &RuntimeConstants.CounterKey, 0, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)

// 添加我们自己 layoutSubviews 的实现
let layoutSubviews: @convention(block) (Any?) -> () = { nullableSelf in
guard let _self = nullableSelf else { return }

let selector = #selector(originalClass.layoutSubviews)
let originalImpl = class_getMethodImplementation(originalClass, selector)

// @convention(c) 告知 Swift 这是一个裸函数指针(没有上下文对象)
// 所有的 Obj-C 方法函数把接收者和消息当作前两个参数
// 所以这意味着一个类型为 `() -> Void` 的方法,这与 `layoutSubview` 方法相符
typealias ObjCVoidVoidFn = @convention(c) (Any, Selector) -> Void
let originalLayoutSubviews = unsafeBitCast(originalImpl, to: ObjCVoidVoidFn.self)
originalLayoutSubviews(view, selector)

if let counter = objc_getAssociatedObject(_self, &RuntimeConstants.CounterKey) as? Int {
if counter == threshold {
onLoop()
}

objc_setAssociatedObject(view, &RuntimeConstants.CounterKey, counter+1, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}

// 为重置计数器,在每个新的 run loop 遍历中分发 work item
if let previousResetWorkItem = objc_getAssociatedObject(view, &RuntimeConstants.ResetWorkItemKey) as? DispatchWorkItem {
previousResetWorkItem.cancel()
}
let counterResetWorkItem = DispatchWorkItem { [weak view] in
guard let strongView = view else { return }
objc_setAssociatedObject(strongView, &RuntimeConstants.CounterKey, 0, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
DispatchQueue.main.async(execute: counterResetWorkItem)
objc_setAssociatedObject(view, &RuntimeConstants.ResetWorkItemKey, counterResetWorkItem, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
let implementation = imp_implementationWithBlock(layoutSubviews)
class_addMethod(trackableClass, #selector(originalClass.layoutSubviews), implementation, “v@:“)
} else if let trackableClass = NSClassFromString(classFullName) {
// 我们之前在此运行时会话中分配了一个具有相同名称的类。
// 我们可以从原始字符串中获取它,并以相同的方式与我们的视图交换。
object_setClass(view, trackableClass)
}
}
}

结论

是的!现在你可以为所有可疑的视图设置分析事件了,发布应用程序,并找到这个问题的确切出处。你可以把这个问题的范围缩小到某个特定的视图,并在用户不知情的情况下借助于他们来解决这个问题。

最后要提到的一件事是:能力越大责任越大。运行时编程非常容易出错,因此很容易在不知情的情况下为应用程序引入另一个严重的问题。这就是为什么总是建议将应用程序中的所有危险代码包装在某种可停止开关中,因为你可以在发现代码导致问题时从后端触发开关禁用该功能。这有一篇介绍 Firebase 的 Feature Flags 的 好文章

完整代码可以从这个 GitHub 仓库 里获取,并且也将会发布到 CocoPods 上,以跟踪项目中的布局循环。

附:我想特别感谢 Aleksandr Gusev 帮助审阅并且为本文提供了很多意见。

本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 http://swift.gg

分析复杂度

作者:Soroush Khanlou,原文链接,原文日期:2018-12-17
译者:RocZhang;校对:numbbbbbWAMaker;定稿:Pancf

在 Dave Abraham 的 WWDC 演讲 Embracing Algorithms(拥抱算法)中,谈到了要发现通用的算法,并将其提取到通用且可测试的函数中。在这个方向上,我发现一些对集合类型的多次操作可以被聚齐起来,合并成单次操作。并且通常情况下,这些操作被合并之后还能带来性能上的收获。

第一个例子是 Swift 3 添加的一个方法:

// 当你看到:
myArray.filter({ /* some test */ }).first

// 你应该把它改成:
myArray.first(where: { /* some test */ })

这里两种写法的断言描述闭包和操作结果都完全相同,但下面的写法更简短,语义更清晰,而且性能更好。因为它不会进行新数组的分配,也不需要对数组中每一个元素是否能够通过测试都进行判断,只需要找出第一个就好了。

另一个例子是 我帮助添加到 Swift 5 中的 count(where:) 函数:

// 当你看到:
myArray.filter({ /* some test */ }).count

// 你应该把它改成:
myArray.count(where: { /* some test */ })

这又是一个更短、更清晰而且更快的例子。没有额外要被分配的数组,也没有多余的操作。

在我们的一个项目中,有一个通用的范式,需要先将集合进行 sort,随后再进行 prefix 操作。例如下述的示例代码,需要找出前 20 张最新创建的图像:

return images
.sorted(by: { $0.createdAt > $1.createdAt })
.prefix(20)

同样,也可以想象成在排行榜中找到前 5 位得分最高的用户,也需要使用这类范式。

我盯着这段代码直到我的眼睛开始流血,感到这段代码可能存在问题。我首先想到的是分析它的时间复杂度。如果把原始数组的长度用 n 表示,再把最后想要得到的元素的数目用 m 表示,在分析代码之后可以得出,排序的复杂度是 O(nlogn),取前子集合的操作则更快,时间复杂度为 O(1)(取前子集合操作本身最慢时可能会达到 O(m),但对这里我们要处理的数组而言,由于它是可随机访问的集合,因此取前子集合操作能在常数时间中完成)。

这正是让我感到困惑的地方:获取一个序列的最小元素(使用 min() 函数)只需要单次遍历所有元素,或者说时间复杂度为 O(n)。将其所有元素进行完整排序需要的时间复杂度是 O(nlogn)。而从集合中获取 m 个数,当 m 比 n 小时,时间复杂度应该位于它们之间。且当 m 比 n 小非常多时,时间复杂度应该更接近 O(n)。

在我们的例子里,图片的数量会非常大(n 约为 55000),而我们想得到的元素数量却很小(m 为 20)。因此,这里应该存在有优化的空间。我们是否能够优化排序,使其仅排序出前 m 个元素?

答案是肯定的,我们能够在这个方向上进行一些优化。我将此函数命名为 smallest(_:by:),它接收 sortprefix 函数的所有参数,也就是上面提到的 m 和用于排序做比较的闭包:

func smallest(_ m: Int, by areInIncreasingOrder: (Element, Element) -> Bool) -> [Element] {

首先从排序前 m 个元素开始(因为 m 比序列的总长度要小很多,所以此操作会进行的很快):

var result = self.prefix(m).sorted(by: areInIncreasingOrder)

然后我们再遍历所有剩下的元素:

for element in self.dropFirst(m) {

对集合中剩下的每一个元素,我们需要找到 result 中第一个比它大的项的索引。通过 areInIncreasingOrder 函数,我们把 element 作为第一个参数传入,再把 result 中的元素作为第二个参数传入。

if let insertionIndex = result.index(where: { areInIncreasingOrder(element, $0) }) { // 译者注:此方法在 Swift 4.2 后已更名为 `firstIndex(where:)`

如果能够找到符合条件的索引值,这就表示存在有比我们 result 数组中的元素更小的值。我们把新的值插入到计算出的索引的位置,它便会被正确的排序:

result.insert(element, at: insertionIndex)

再将最后一个元素移除(因为我们只需要 m 个元素):

result.removeLast()

如果没有找到满足条件的索引,我们就可以忽略这个值。最后,当 for 循环完成,便可将 result 返回。

完整的函数如下所示:

extension Sequence {
func smallest(_ m: Int, by areInIncreasingOrder: (Element, Element) -> Bool) -> [Element] {
var result = self.prefix(m).sorted(by: areInIncreasingOrder)

for e in self.dropFirst(n) {
if let insertionIndex = result.index(where: { areInIncreasingOrder(e, $0) }) {
result.insert(e, at: insertionIndex)
result.removeLast()
}
}

return result
}
}

如果这让你想起了之前在计算机科学中学过的课程,那就再好不过了。实际上这里的算法就类似于选择排序的过程(但它们并非完全相同,因为这里会预先排序一部分元素,而选择排序则不同,是可变序列算法(mutating algorithm))。

这里的时间复杂度分析起来可能会有些困难,但是我们还是可以尝试进行分析。初始部分的排序是 O(mlogm),外层的循环是 O(n)。每次的循环中,会分别调用时间复杂度都为 O(m) 的 index(where:)insert(_:at:)(插入操作的时间复杂度是 O(m) 的原因在于,它可能需要将所有的元素后移,为新元素腾出空间)。因此,总时间复杂度应为 O(mlogm + n * (m + m)),或者说 O(mlogm + 2nm)。常数项被移除后,留下的则是 O(mlogm + nm)。

当 m 比 n 小得多时,m 项会接近于常数,最终我们得到的会是 O(n)。相较于之前的 O(nlogn) 而言,这是一个巨大的改进。对应到之前提到的 55000 张图片的例子,这可能会是多达 5 倍的性能提升。

大体上来说,这里的函数是对 prefix + sort 函数的优化。但我还想要再讨论两处更细小一些的优化。

一处唾手可得的优化是:我们是在 55000 个元素的数组中查找 20 个最小的元素,其中我们检查的大部分(几乎是全部)元素不会落入到最后的 result 数组中。因此我们可以去检查元素是否比 result 数组中的最后一个元素要大,如果是,它就完全可以被跳过。因为当元素比 result 中的最后一个还要大时,再去查找插入的索引就没有任何意义了。

if let last = result.last, areInIncreasingOrder(last, e) { continue }

在测试中,此处增加的判断可以减少线性搜索 result 数组 99.5% 的时间,算法整体上又会加快十倍左右。感谢 Greg Titus 告诉我此处可以优化──之前我完全没有想到这一点。

如果想更近一步的话,还可以做另一处(稍微难实现一些)的优化。此优化基于两处事实:第一,我们使用 index(where:) 来找出应在 result 数组中进行插入的位置;第二,result 数组总是保持有序的。index(where:) 通常情况下是一项线性操作,但如果是在一个已经排好序的数组中进行搜索,我们可以将线性搜索替换成二分搜索。我对此进行了尝试。

为了能够更好的理解这些优化会如何影响算法的性能,Nate Cook 帮助我了解了 Karoy Lorentey 的 Attabench 工具,它能够对这些解决方案进行基准测试。因为截止目前,我们对复杂度的分析都是停留在理论层面的,在真正对代码进行实际测试之前(最理想的情况应该是在真实的设备上),所有的结论都只是有根据的推测。例如,通常来说排序的复杂度为 O(nlogn),但不同的算法处理不同类型的数据时,其表现也会有所不同。具体来说,已经排好序的数据在特定的算法中可能会变得更快或更慢。

Attabench 的执行结果如下:

(我还添加了一个 由 Time Vermuelen 所写的优先队列/堆解决方案,因为有些人好奇它与其他方案比较起来表现如何。)

首先,我对在数组中进行单次搜索比对数组进行完整排序要快的猜测是正确的。尤其是在实际问题中序列可能会很长,排序的性能则会变得更差,但我们的“简单优化”(图中的 “Naive Optimization”)却能保持在常数水平上(Y 轴表示的是单个元素上所花的时间,而不是总时间。这意味着 O(n) 的算法在图中会是一条直线)。

第二,对最后一个元素的检查(图中的 “Last Check”)和二分搜索优化在独立运行时具有几乎完全相同的性能表现(实际上你可能没法看到橘色和黄色的线,因为它们被绿线挡住了),把它们放在一起使用时也是一样。但是由于二分搜索难以编写,甚至更难把它写对,你也可以说把它加上是不值得的。

对我而言,这里传递出的关键信息是测量和优化很难。虽然分析复杂度这件事听起来有些学术:“我什么时候会在自己的职业生涯上用到这个?” 有人会问。但理解你的算法的时间和空间复杂度能够帮助你决定向哪个方向进行探索。在这个例子中,理解排序的时间复杂度引导我们对问题产生了概括性的认知,得到成果。最后,通过使用各种数据进行的进一步的基准测试与分析,能告诉我们代码在生产环境下将如何运作的最准确的信息。

下一次再看到 sort 后面紧跟着一个 prefix 时,不妨考虑将它替换成 smallest(_: by:)

本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 http://swift.gg

图像渲染优化技巧

作者:Mattt,原文链接,原文日期:2019-05-06
译者:ericchuhong;校对:numbbbbbWAMaker;定稿:Pancf

长期以来,iOS 开发人员一直被一个奇怪的问题困扰着:

“如何对一张图像进行渲染优化?”

这个令人困扰的问题,是由于开发者和平台的相互不信任引起的。各种各样的代码示例充斥着 Stack Overflow,每个人都声称只有自己的方法是真正的解决方案 —— 而别人的是错的。

在本周的文章中,我们将介绍 5 种不同的 iOS 图像渲染优化技巧(在 MacOS 上时适当地将 UIImage 转换成 NSImage)。相比于对每一种情况都规定一种方法,我们将从人类工程学和性能表现方面进行衡量,以便你更好地理解什么时该用哪一种,不该用哪一些。

你可以自己下载、构建和运行 示例项目代码 来试验这些图像渲染优化技巧。


图像渲染优化的时机和理由

在开始之前,让我们先讨论一下为什么需要对图像进行渲染优化。毕竟,UIImageView 会自动根据 contentmode 属性 规定的行为缩放和裁剪图像。在绝大多数情况下,.scaleAspectFit.scaleAspectFill.scaleToFill 已经完全满足你的所需。

imageView.contentMode = .scaleAspectFit
imageView.image = image

那么,什么时候对图像进行渲染优化才有意义呢?

当它明显大于 UIImageView 显示尺寸的时候


看看来自 NASA 视觉地球相册集锦 的这张 令人赞叹的图片

image-resizing-earth

想要完整渲染这张宽高为 12,000 px 的图片,需要高达 20 MB 的空间。对于当今的硬件来说,你可能不会在意这么少兆字节的占用。但那只是它压缩后的尺寸。要展示它,UIImageView 首先需要把 JPEG 数据解码成位图(bitmap),如果要在一个 UIImageView 上按原样设置这张全尺寸图片,你的应用内存占用将会激增到几百兆,对用户明显没有什么好处(毕竟,屏幕能显示的像素有限)。但只要在设置 UIImageViewimage 属性之前,将图像渲染的尺寸调整成 UIImageView 的大小,你用到的内存就会少一个数量级:

内存消耗 (MB)
无下采样 220.2
下采样 23.7

这个技巧就是众所周知的下采样(downsampling),在这些情况下,它可以有效地优化你应用的性能表现。如果你想了解更多关于下采样的知识或者其它图形图像的最佳实践,请参照 来自 WWDC 2018 的精彩课程

而现在,很少有应用程序会尝试一次性加载这么大的图像了,但是也跟我从设计师那里拿到的图片资源不会差多。(认真的吗?一张颜色渐变的 PNG 图片要 3 MB?) 考虑到这一点,让我们来看看有什么不同的方法,可以让你用来对图像进行优化或者下采样。

不用说,这里所有从 URL 加载的示例图像都是针对本地文件。记住,在应用的主线程同步使用网络请求图像绝不是什么好主意。


图像渲染优化技巧

优化图像渲染的方法有很多种,每种都有不同的功能和性能特性。我们在本文看到的这些例子,架构层次跨度上从底层的 Core Graphics、vImage、Image I/O 到上层的 Core Image 和 UIKit 都有。

  1. 绘制到 UIGraphicsImageRenderer 上
  2. 绘制到 Core Graphics Context 上
  3. 使用 Image I/O 创建缩略图像
  4. 使用 Core Image 进行 Lanczos 重采样
  5. 使用 vImage 优化图片渲染

为了统一调用方式,以下的每种技术共用一个公共接口方法:

func resizedImage(at url: URL, for size: CGSize) -> UIImage? { <#...#> }

imageView.image = resizedImage(at: url, for: size)

这里,size 的计量单位不是用 pixel,而是用 point。想要计算出你调整大小后图像的等效尺寸,用主 UIScreenscale,等比例放大你 UIImageViewsize 大小:

let scaleFactor = UIScreen.main.scale
let scale = CGAffineTransform(scaleX: scaleFactor, y: scaleFactor)
let size = imageView.bounds.size.applying(scale)

如果你是在异步加载一张大图,使用一个过渡动画让图像逐渐显示到 UIImageView 上。例如:

class ViewController: UIViewController {
@IBOutlet var imageView: UIImageView!

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)

let url = Bundle.main.url(forResource: "Blue Marble West",
withExtension: "tiff")!

DispatchQueue.global(qos: .userInitiated).async {
let image = resizedImage(at: url, for: self.imageView.bounds.size)

DispatchQueue.main.sync {
UIView.transition(with: self.imageView,
duration: 1.0,
options: [.curveEaseOut, .transitionCrossDissolve],
animations: {
self.imageView.image = image
})
}
}
}
}

技巧 #1: 绘制到 UIGraphicsImageRenderer 上

图像渲染优化的最上层 API 位于 UIKit 框架中。给定一个 UIImage,你可以绘制到 UIGraphicsImageRenderer 的上下文(context)中以渲染缩小版本的图像:

import UIKit

// 技巧 #1
func resizedImage(at url: URL, for size: CGSize) -> UIImage? {
guard let image = UIImage(contentsOfFile: url.path) else {
return nil
}

let renderer = UIGraphicsImageRenderer(size: size)
return renderer.image { (context) in
image.draw(in: CGRect(origin: .zero, size: size))
}
}

UIGraphicsImageRenderer 是一项相对较新的技术,在 iOS 10 中被引入,用以取代旧版本的 UIGraphicsBeginImageContextWithOptions / UIGraphicsEndImageContext API。你通过指定以 point 计量的 size 创建了一个 UIGraphicsImageRendererimage 方法带有一个闭包参数,返回的是一个经过闭包处理后的位图。最终,原始图像便会在缩小到指定的范围内绘制。

在不改变图像原始纵横比(aspect ratio)的情况下,缩小图像原始的尺寸来显示通常很有用。AVMakeRect(aspectRatio:insideRect:) 是在 AVFoundation 框架中很方便的一个函数,负责帮你做如下的计算:

import func AVFoundation.AVMakeRect
let rect = AVMakeRect(aspectRatio: image.size, insideRect: imageView.bounds)

技巧 #2:绘制到 Core Graphics Context 中

Core Graphics / Quartz 2D 提供了一系列底层 API 让我们可以进行更多高级的配置。

给定一个 CGImage 作为暂时的位图上下文,使用 draw(_:in:) 方法来绘制缩放后的图像:

import UIKit
import CoreGraphics

// 技巧 #2
func resizedImage(at url: URL, for size: CGSize) -> UIImage? {
guard let imageSource = CGImageSourceCreateWithURL(url as NSURL, nil),
let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil)
else {
return nil
}

let context = CGContext(data: nil,
width: Int(size.width),
height: Int(size.height),
bitsPerComponent: image.bitsPerComponent,
bytesPerRow: image.bytesPerRow,
space: image.colorSpace ?? CGColorSpace(name: CGColorSpace.sRGB)!,
bitmapInfo: image.bitmapInfo.rawValue)
context?.interpolationQuality = .high
context?.draw(image, in: CGRect(origin: .zero, size: size))

guard let scaledImage = context?.makeImage() else { return nil }

return UIImage(cgImage: scaledImage)
}

这个 CGContext 初始化方法接收了几个参数来构造一个上下文,包括了必要的宽高参数,还有在给出的色域范围内每个颜色通道所需要的内存大小。在这个例子中,这些参数都是通过 CGImage 这个对象获取的。下一步,设置 interpolationQuality 属性为 .high 指示上下文在保证一定的精度上填充像素。draw(_:in:) 方法则是在给定的宽高和位置绘制图像,可以让图片在特定的边距下裁剪,也可以适用于一些像是人脸识别之类的图像特性。最后 makeImage() 从上下文获取信息并且渲染到一个 CGImage 值上(之后会用来构造 UIImage 对象)。

技巧 #3:使用 Image I/O 创建缩略图像

Image I/O 是一个强大(却鲜有人知)的图像处理框架。抛开 Core Graphics 不说,它可以读写许多不同图像格式,访问图像的元数据,还有执行常规的图像处理操作。这个框架通过先进的缓存机制,提供了平台上最快的图片编码器和解码器,甚至可以增量加载图片。

这个重要的 CGImageSourceCreateThumbnailAtIndex 提供了一个带有许多不同配置选项的 API,比起在 Core Graphics 中等价的处理操作要简洁得多:

import ImageIO

// 技巧 #3
func resizedImage(at url: URL, for size: CGSize) -> UIImage? {
let options: [CFString: Any] = [
kCGImageSourceCreateThumbnailFromImageIfAbsent: true,
kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceShouldCacheImmediately: true,
kCGImageSourceThumbnailMaxPixelSize: max(size.width, size.height)
]

guard let imageSource = CGImageSourceCreateWithURL(url as NSURL, nil),
let image = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary)
else {
return nil
}

return UIImage(cgImage: image)
}

给定一个 CGImageSource 和一系列配置选项,CGImageSourceCreateThumbnailAtIndex(_:_:_:) 函数创建了一个图像的缩略图。优化尺寸大小的操作是通过 kCGImageSourceThumbnailMaxPixelSize 完成的,它根据图像原始宽高比指定的最大尺寸来缩放图像。通过设定 kCGImageSourceCreateThumbnailFromImageIfAbsentkCGImageSourceCreateThumbnailFromImageAlways 选项,Image I/O 可以自动缓存优化后的结果以便后续调用。

技巧 #4:使用 Core Image 进行 Lanczos 重采样

Core Image 内置了 Lanczos 重采样(resampling) 功能,它是以 CILanczosScaleTransform 的同名滤镜命名的。虽然可以说它是在 UIKit 层级之上的 API,但无处不在的 key-value 编写方式导致它使用起来很不方便。

即便如此,它的处理模式还是一致的。

创建转换滤镜,对滤镜进行配置,最后渲染输出图像,这样的步骤和其他任何 Core Image 的工作流没什么不同。

import UIKit
import CoreImage

let sharedContext = CIContext(options: [.useSoftwareRenderer : false])

// 技巧 #4
func resizedImage(at url: URL, scale: CGFloat, aspectRatio: CGFloat) -> UIImage? {
guard let image = CIImage(contentsOf: url) else {
return nil
}

let filter = CIFilter(name: "CILanczosScaleTransform")
filter?.setValue(image, forKey: kCIInputImageKey)
filter?.setValue(scale, forKey: kCIInputScaleKey)
filter?.setValue(aspectRatio, forKey: kCIInputAspectRatioKey)

guard let outputCIImage = filter?.outputImage,
let outputCGImage = sharedContext.createCGImage(outputCIImage,
from: outputCIImage.extent)
else {
return nil
}

return UIImage(cgImage: outputCGImage)
}

这个名叫 CILanczosScaleTransform 的 Core Image 滤镜分别接收了 inputImageinputScaleinputAspectRatio 三个参数,每一个参数的意思也都不言自明。

更有趣的是,CIContext 在这里被用来创建一个 UIImage(间接通过 CGImageRef 表示),因为 UIImage(CIImage:) 经常不能按我们本意使用。创建 CIContext 是一个代价很昂贵的操作,所以使用上下文缓存以便重复的渲染工作。

一个 CIContext 可以使用 GPU 或者 CPU(慢很多)渲染创建出来。通过指定构造方法中的 .useSoftwareRenderer 选项来选择使用哪个硬件。(提示:用更快的那个,你觉得呢?)

技巧 #5: 使用 vImage 优化图片渲染

最后一个了,它是古老的 Accelerate 框架 —— 更具体点来说,它是 vImage 的图像处理子框架。

vImage 附带有 一些不同的功能,可以用来裁剪图像缓冲区大小。这些底层 API 保证了高性能同时低能耗,但会导致你对缓冲区的管理操作增加(更不用说要编写更多的代码了):

import UIKit
import Accelerate.vImage

// 技巧 #5
func resizedImage(at url: URL, for size: CGSize) -> UIImage? {
// 解码源图像
guard let imageSource = CGImageSourceCreateWithURL(url as NSURL, nil),
let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil),
let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [CFString: Any],
let imageWidth = properties[kCGImagePropertyPixelWidth] as? vImagePixelCount,
let imageHeight = properties[kCGImagePropertyPixelHeight] as? vImagePixelCount
else {
return nil
}

// 定义图像格式
var format = vImage_CGImageFormat(bitsPerComponent: 8,
bitsPerPixel: 32,
colorSpace: nil,
bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.first.rawValue),
version: 0,
decode: nil,
renderingIntent: .defaultIntent)

var error: vImage_Error

// 创建并初始化源缓冲区
var sourceBuffer = vImage_Buffer()
defer { sourceBuffer.data.deallocate() }
error = vImageBuffer_InitWithCGImage(&sourceBuffer,
&format,
nil,
image,
vImage_Flags(kvImageNoFlags))
guard error == kvImageNoError else { return nil }

// 创建并初始化目标缓冲区
var destinationBuffer = vImage_Buffer()
error = vImageBuffer_Init(&destinationBuffer,
vImagePixelCount(size.height),
vImagePixelCount(size.width),
format.bitsPerPixel,
vImage_Flags(kvImageNoFlags))
guard error == kvImageNoError else { return nil }

// 优化缩放图像
error = vImageScale_ARGB8888(&sourceBuffer,
&destinationBuffer,
nil,
vImage_Flags(kvImageHighQualityResampling))
guard error == kvImageNoError else { return nil }

// 从目标缓冲区创建一个 CGImage 对象
guard let resizedImage =
vImageCreateCGImageFromBuffer(&destinationBuffer,
&format,
nil,
nil,
vImage_Flags(kvImageNoAllocate),
&error)?.takeRetainedValue(),
error == kvImageNoError
else {
return nil
}

return UIImage(cgImage: resizedImage)
}

这里使用 Accelerate API 进行的明确操作,比起目前为止讨论到的其他优化方法更加底层。但暂时不管这些不友好的类型申明和函数名称的话,你会发现这个方法相当直接了当。

  • 首先,根据你传入的图像创建一个输入的源缓冲区,
  • 接着,创建一个输出的目标缓冲区来接受优化后的图像,
  • 然后,在源缓冲区裁剪图像数据,然后传给目标缓冲区,
  • 最后,从目标缓冲区中根据处理完后的图像创建 UIImage 对象。

性能对比

那么这些不同的方法是如何相互对比的呢?

这个项目 是一些 性能对比 结果,运行环境是 iPhone 7 iOS 12.2。

image-resizing-app-screenshot

下面的这些数字是多次迭代加载、优化、渲染之前那张 超大地球图片 的平均时间:

耗时 (seconds)
技巧 #1: UIKit 0.1420
技巧 #2: Core Graphics 1 0.1722
技巧 #3: Image I/O 0.1616
技巧 #4: Core Image 2 2.4983
技巧 #5: vImage 2.3126

1  
设置不同的 CGInterpolationQuality 值出来的结果是一致的,在性能上的差异可以忽略不计。

2  
若在 CIContext 创建时设置 kCIContextUseSoftwareRenderer 的值为 true,会导致耗时相比基础结果慢一个数量级。

总结

  • UIKit, Core Graphics, 和 Image I/O 都能很好地用于大部分图片的优化操作。如果(在 iOS 平台,至少)要选择一个的话,UIGraphicsImageRenderer 是你最佳的选择。
  • Core Image 在图像优化渲染操作方面性能表现优越。实际上,根据 Apple 官方 Core Image 编程规范中的性能最佳实践单元,你应该使用 Core Graphics 或 Image I/O 对图像进行裁剪和下采样,而不是用 Core Image。
  • 除非你已经在使用 vImage,否则在大多数情况下用到底层的 Accelerate API 所需的额外工作可能是不合理的。

本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 http://swift.gg

每一点进步都是快乐:无处不在的扩展

作者:Russ Bishop,原文链接,原文日期:2018-11-08
译者:俊东;校对:numbbbbbWAMaker;定稿:Pancf

这篇文章记录了我所收获的小惊喜。在 Swift 中写扩展让人感觉非常自然。

我认为 UnsafeMutableRawBufferPointer.baseAddress 是可选项这回事非常不合理。在实践中它会使代码变得丑陋。我也不喜欢在分配时指定对齐方式;在大多数平台上,合理的默认值都是 Int.bitWidth / 8

通过扩展,我们可以很容易地解决这些问题。这样的解决方案能像标准库一样自然地使用。

首先,我们需要在调试版本中进行简单的健全性检查,以确保不会产生无意义的对齐计算。这里提一个有关正整数的小技巧:一个 2 的 n 次幂数只有一个比特位是有值的。减去 1 时就是把后面的所有比特位设置为 1,如 8(0b1000)- 1 得到 7(0b0111)。这两个数字没有共同的位,因此按位取与应该产生零。由于这规律在零上无效,所以需要单独检查。

extension BinaryInteger {
var isPositivePowerOf2: Bool {
@inline(__always)
get {
return (self & (self - 1)) == 0 && self != 0
}
}
}

让 allocate 方法默认使用自然整数宽度对齐。设置对齐参数可能有点多余,不过它几乎能处理我们想要存储在缓冲区中的任何数据。虽然断言仅在调试环境中有效,但这已经够应付我们的使用;已知 Swift 支持的平台上这个断言都会是 true。

extension UnsafeMutableRawBufferPointer {
static func allocate(byteCount: Int) -> UnsafeMutableRawBufferPointer {
let alignment = Int.bitWidth / 8
assert(alignment.isPositivePowerOf2, "expected power of two")
return self.allocate(byteCount: byteCount, alignment: alignment)
}
}

最后再提一个点,我们可以添加一个隐式强制解包的 base 属性。

extension UnsafeMutableRawBufferPointer {
var base: UnsafeMutableRawPointer {
return baseAddress!
}
}
extension UnsafeRawBufferPointer {
var base: UnsafeRawPointer {
return baseAddress!
}
}

一切如此简单。

本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 http://swift.gg

Xcode Build 配置文件

作者:Mattt,原文链接,原文日期:2019-05-13
译者:雨谨;校对:numbbbbbWAMaker;定稿:Pancf

软件开发最佳实践 规定了 配置与代码的严格分离。然而,苹果平台上的开发人员常常难以将这些指导原则与 Xcode 繁重的项目工作流程结合起来。

了解每个项目设置的功能以及它们之间如何交互,是一项需要多年磨练的技能。但 Xcode 将大部分的这类信息都都深埋在其图形化界面中,这对我们没有任何好处。

导航到项目编辑器的 “Build Settings” tab,你会看到分布在 project、target 和 configuration 上的 数百条 Build Setting(构建配置) —— 更别说其他六个 tab 了!

幸运的是,有一个更好的办法,不必点击迷宫般的 tab 和箭头,就可以管理所有的配置。

这一周,我们将向你展示如何在 Xcode 之外,通过修改基于文本的 xcconfig 文件,让你的项目更加紧凑、易懂、强大。


Xcode Build 配置文件,即大家所熟知的 xcconfig 文件,允许我们在不使用 Xcode 的情况下声明和管理 APP 的 Build Setting。它们是纯文本,这意味着它们对代码管理系统更加友好,而且可以被任意编辑器修改。

从根本上说,每个配置文件都由一系列键值对组成,其语法如下:

<#BUILD_SETTING_NAME#> = <#value#>

例如,你可以使用下面这样的 SWIFT_VERSION Build Setting,指定项目的 Swift 语言版本:

SWIFT_VERSION = 5.0

根据 POSIX 标准,环境变量的名字由全大写字母、数字和下划线(_)组成 —— 经典例子就是 SCREAMING_SNAKE_CASE 🐍🗯。


乍一看,xcconfig 文件与 .env 文件有惊人的相似之处,它们的语法都很简单,都以换行分隔。但是,Xcode Build 配置文件的内容比表面上看到的要多。看哪!

保留现有值

要追加新内容,而不是替换现有定义时,可以像这样使用 $(inherited) 变量:

<#BUILD_SETTING_NAME#> = $(inherited)<#additional value#>

这么做通常是为了搭建一些值的列表,比如编译器的 framework 头文件的搜索路径(FRAMEWORK_SEARCH_PATHS):

FRAMEWORK_SEARCH_PATHS = $(inherited) $(PROJECT_DIR)

Xcode 按下面的顺序对 inherited 进行赋值(优先级从低到高):

  • 平台默认值(Platform Defaults)
  • Xcode 项目文件的 Build Setting(Xcode Project File Build Settings)
  • Xcode 项目的 xcconfig 文件(xcconfig File for the Xcode Project)
  • Active Target 的 Build Setting(Active Target Build Settings)
  • Active Target 的 xcconfig 文件(xcconfig File for the Active Target)

空格用于分隔字符串和路径列表中的项。指定包含空格的项时,必须用引号(")括起来。

引用其他值

你可以按照下面的语法,通过其他设置的名字引用它们的值:

<#BUILD_SETTING_NAME#> = $(<#ANOTHER_BUILD_SETTING_NAME#>)

这种引用既可以用于根据现有值定义新变量,也可以用于以内联方式动态构建新值。

OBJROOT = $(SYMROOT)
CONFIGURATION_BUILD_DIR = $(BUILD_DIR)/$(CONFIGURATION)-$(PLATFORM_NAME)

条件约束

使用以下语法,你可以按 SDK(sdk)、架构(arch)和 / 或配置(config)对 Build Setting 进行条件约束:

<#BUILD_SETTING_NAME#>[sdk=<#sdk#>] = <#value for specified sdk#>
<#BUILD_SETTING_NAME#>[arch=<#architecture#>] = <#value for specified architecture#>
<#BUILD_SETTING_NAME#>[config=<#configuration#>] = <#value for specified configuration#>

如果需要在同一 Build Setting 的多个定义之间进行选择,编译器将根据条件约束进行解析。

<#BUILD_SETTING_NAME#>[sdk=<#sdk#>][arch=<#architecture#>] = <#value for specified sdk and architectures#>
<#BUILD_SETTING_NAME#>[sdk=*][arch=<#architecture#>] = <#value for all other sdks with specified architecture#>

例如,你可以使用下面这条 Build Setting 指定仅编译 active architecture,从而提升本地 Build 的速度。

ONLY_ACTIVE_ARCH[config=Debug][sdk=*][arch=*] = YES

引用其他配置文件中的设置

C 语言的 #include 指令一样,Build 配置文件也可以使用这种语法来引用其他配置文件中的设置。

#include "<#path/to/File.xcconfig#>"

正如我们将在本文后面看到的,你可以利用这一点,以非常强大的方式搭建起 Build Setting 的级联列表。

正常来说,当遇到一个无法解析的 #include 指令时,编译器会报错。但是 xcconfig 文件同时也支持 #include? 指令,在该指令下,若文件无法找到,编译器不会报错。

根据文件是否存在而改变编译时行为的情况并不多;毕竟,Build 最好是可预见的。但是你可以把它用在可选的开发工具上,比如 Reveal 需要以下的配置:

> # Reveal.xcconfig
> OTHER_LDFLAGS = $(inherited) -weak_framework RevealServer
> FRAMEWORK_SEARCH_PATHS = $(inherited) /Applications/Reveal.app/Contents/SharedSupport/iOS-Libraries
>

创建 Build 配置文件

要创建 Build 配置文件,请选择 “File > New File…” 菜单项(N),下拉到 “Other” 部分,选中 Configuration Settings File 模板。将它保存到你的项目目录,并确保它在你期望的 target 上。

创建好 xcconfig 文件后,你就可以将它分配给对应 target 的一个或多个 Build 配置。


现在我们已经介绍了 Xcode Build 配置文件使用的基础知识,那么让我们来看几个示例,看看如何使用它们来管理 development、stage 和 production 环境。


为内部版本提供自定义的 APP 名称和图标

开发 iOS 应用程序时,通常需要在模拟器和测试设备上安装各种内部版本(同时也会安装应用程序商店的最新版本,以供参考)。

使用 xcconfig 文件,你可以轻松地为每个配置分配一个不同的名称和 APP 图标。

// Development.xcconfig
PRODUCT_NAME = $(inherited) α
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon-Alpha

---

// Staging.xcconfig
PRODUCT_NAME = $(inherited) β
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon-Beta

管理不同环境下的常量

如果你的后端开发人员也遵循前面提到的 12 Factor App 理论,那么他们将为 development、stage 和 production 环境提供单独的接口。

iOS 上最常见的环境管理方式可能就是使用条件编译语句 + DEBUG 这样的 Build Setting 了。

import Foundation

#if DEBUG
let apiBaseURL = URL(string: "https://api.example.dev")!
let apiKey = "9e053b0285394378cf3259ed86cd7504"
#else
let apiBaseURL = URL(string: "https://api.example.com")!
let apiKey = "4571047960318d233d64028363dfa771"
#endif

这只是完成了任务,但是与代码 / 配置分离的标准相冲突。

另一个方案是将这些与环境相关的值放到它们该待的地方 —— xcconfig 文件中。

// Development.xcconfig
API_BASE_URL = api.example.dev
API_KEY = 9e053b0285394378cf3259ed86cd7504

---

// Production.xcconfig
API_BASE_URL = api.example.com
API_KEY = 4571047960318d233d64028363dfa771

不幸的是,xcconfig 将所有 // 都当成注释分隔符,不管它们是否包括在引号中。如果你用反斜线 \/\/ 进行转义,那么这些反斜线也将被直接展示出现,使用时必须从结果中移除。在指定每个环境的 URL 常量时,这尤其不方便。

如果不想处理这种麻烦的事情,你可以在 xcconfig 中忽略 scheme,然后在代码中添加 https://(你是在使用 https……对吧?)

然而,要以编程方式获取这些值,我们还需要一个额外的步骤:

在 Swift 中访问 Build Setting

由 Xcode 项目文件、xcconfig 文件和环境变量定义的 Build Setting 只在 Build 时可用。当你运行一个已经编译的 APP 时,所有相关的上下文都是不可见的。(谢天谢地!)

但是等一下——你不记得之前在其他 tab 中看到过一些 Build Setting 吗?Info,是吗?

实际上,Info tab 只是 target 的 Info.plist 文件的一个马甲。Build 时,这个 Info.plist 文件会根据 Build Setting 的配置进行编译,然后复制到最终 APP 的 bundle 中。因此,添加 $(API_BASE_URL)$(API_KEY) 的引用后,你可以通过 Foundation Bundle API 的 infoDictionary 属性访问这些值。完美!

按照这种方法,我们可以做如下工作:

import Foundation

enum Configuration {
static func value<T>(for key: String) -> T {
guard let value = Bundle.main.infoDictionary?[key] as? T else {
fatalError("Invalid or missing Info.plist key: \(key)")
}

return value
}
}

enum API {
static var baseURL: URL {
return URL(string: "https://" + Configuration.value(for: "API_BASE_URL"))!
}

static var key: String {
return Configuration.value(for: "API_KEY")
}
}

从调用的角度考虑,我们发现这种方法与我们的最佳实践完美地在结合一起 —— 没有出现一个硬编码的常量!

let url = URL(string: path, relativeTo: API.baseURL)!
var request = URLRequest(url: url)
request.httpMethod = method
request.addValue(API.key, forHTTPHeaderField: "X-API-KEY")

不要把私密的东西写在代码中。相反,应该将它们安全地存储在密码管理器或类似的东西中。

为了防止你的私密被泄漏到 GitHub 上,请将下列配置添加到你的 .gitignore 文件中(根据需要):

> # .gitignore
> Development.xcconfig
> Staging.xcconfig
> Production.xcconfig
>

一些开发人员喜欢使用包含了所需 key 的占位符文件(例如 Development.sample.xcconfig)代替这些文件。拉取代码时,开发人员再将该文件复制到非占位符位置,并相应地填充它。



Xcode 项目是庞大、脆弱的和不透明的。它们是团队成员合作时摩擦的来源,也常常是工作的累赘。

幸运的是,xcconfig 文件很好地解决了这些痛点。将配置从 Xcode 移到 xcconfig 文件带来了很多好处,可以让你的项目与 Xcode 的细节保持一定距离,不受苹果公司的掣肘。

本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 http://swift.gg

Swift 5 字符串插值-AttributedStrings

作者:Olivier Halligon,原文链接,原文日期:2018-12-16
译者:Nemocdz;校对:numbbbbbWAMaker;定稿:Pancf


我们已经在 前文 里介绍了 Swift 5 全新的 StringInterpolation 设计。在这第二部分中,我会着眼于 ExpressibleByStringInterpolation 其中一种应用,让 NSAttributedString 变得更优雅。

目标

在看到 Swift 5 这个全新的 StringInterpolation 设计 时,我马上想到的应用之一就是简化 NSAttributedString 的生成。

我的目标是做到用类似下面的语法创建一个 attributed 字符串:

let username = "AliGator"
let str: AttrString = """
Hello \(username, .color(.red)), isn't this \("cool", .color(.blue), .oblique, .underline(.purple, .single))?

\(wrap: """
\(" Merry Xmas! ", .font(.systemFont(ofSize: 36)), .color(.red), .bgColor(.yellow))
\(image: #imageLiteral(resourceName: "santa.jpg"), scale: 0.2)
""", .alignment(.center))

Go there to \("learn more about String Interpolation", .link("https://github.com/apple/swift-evolution/blob/master/proposals/0228-fix-expressiblebystringinterpolation.md"), .underline(.blue, .single))!
"""

这一大串字符串不仅使用了多行字符串的字面量语法(顺带一提,这个特性是在 Swift4 中新增的,以免你错过了) ——而且在其中一个多行字符串字面量中包含了另一个(见 \(wrap: ...) 段落)!- 甚至还包含了给一部分字符添加一些样式的插值……所以由大量 Swift 新特性组合而成!

这个 NSAttributedString 如果在一个 UILabel 或者 NSTextView 中渲染,结果是这个样子的:

image

☝️ 是的,上面的文字和图片……真的是一个 NSAttributedString(而不是一个复杂的视图布局或者其他)! 🤯

初步实现

所以,从哪里开始实现?当然和第一部分中如何实现 GitHubComment 差不多!

好的,在实际解决字符串插值之前,我们先从声明特有类型开始。

struct AttrString {
let attributedString: NSAttributedString
}

extension AttrString: ExpressibleByStringLiteral {
init(stringLiteral: String) {
self.attributedString = NSAttributedString(string: stringLiteral)
}
}

extension AttrString: CustomStringConvertible {
var description: String {
return String(describing: self.attributedString)
}
}

挺简单的吧?仅仅给 NSAttributedString 封装了一下。现在,让我们添加 ExpressibleByStringInterpolation 的支持,来同时支持字面量和带 NSAttributedString 属性注释的字符串。

extension AttrString: ExpressibleByStringInterpolation {
init(stringInterpolation: StringInterpolation) {
self.attributedString = NSAttributedString(attributedString: stringInterpolation.attributedString)
}

struct StringInterpolation: StringInterpolationProtocol {
var attributedString: NSMutableAttributedString

init(literalCapacity: Int, interpolationCount: Int) {
self.attributedString = NSMutableAttributedString()
}

func appendLiteral(_ literal: String) {
let astr = NSAttributedString(string: literal)
self.attributedString.append(astr)
}

func appendInterpolation(_ string: String, attributes: [NSAttributedString.Key: Any]) {
let astr = NSAttributedString(string: string, attributes: attributes)
self.attributedString.append(astr)
}
}
}

这时,已经可以用下面这种方式简单地构建一个 NSAttributedString 了:

let user = "AliSoftware"
let str: AttrString = """
Hello \(user, attributes: [.foregroundColor: NSColor.blue])!
"""

这看起来已经优雅多了吧?

方便的样式添加

但用字典 [NAttributedString.Key: Any] 的方式处理属性不够优雅。特别是由于 Any 没有明确类型,要求了解每一个键值的明确类型……

所以可以通过创建特有的 Style 类型让它变得更优雅,并帮助我们构建属性的字典:

extension AttrString {
struct Style {
let attributes: [NSAttributedString.Key: Any]
static func font(_ font: NSFont) -> Style {
return Style(attributes: [.font: font])
}
static func color(_ color: NSColor) -> Style {
return Style(attributes: [.foregroundColor: color])
}
static func bgColor(_ color: NSColor) -> Style {
return Style(attributes: [.backgroundColor: color])
}
static func link(_ link: String) -> Style {
return .link(URL(string: link)!)
}
static func link(_ link: URL) -> Style {
return Style(attributes: [.link: link])
}
static let oblique = Style(attributes: [.obliqueness: 0.1])
static func underline(_ color: NSColor, _ style: NSUnderlineStyle) -> Style {
return Style(attributes: [
.underlineColor: color,
.underlineStyle: style.rawValue
])
}
static func alignment(_ alignment: NSTextAlignment) -> Style {
let ps = NSMutableParagraphStyle()
ps.alignment = alignment
return Style(attributes: [.paragraphStyle: ps])
}
}
}

这允许使用 Style.color(.blue) 来简单地创建一个封装了 [.foregroundColor: NSColor.blue]Style

可别止步于此,现在让我们的 StringInterpolation 可以处理下面这样的 Style 属性!

这个想法是可以做到像这样写:

let str: AttrString = """
Hello \(user, .color(.blue)), how do you like this?
"""

是不是更优雅?而我们仅仅需要为它正确实现 appendInterpolation 而已!

extension AttrString.StringInterpolation {
func appendInterpolation(_ string: String, _ style: AttrString.Style) {
let astr = NSAttributedString(string: string, attributes: style.attributes)
self.attributedString.append(astr)
}

然后就完成了!但……这样一次只支持一个 Style。为什么不允许它传入多个 Style 作为形参呢?这可以用一个 [Style] 形参来实现,但这要求调用侧将样式列表用括号括起来……不如让它使用可变形参?

让我们用这种方式来代替之前的实现:

extension AttrString.StringInterpolation {
func appendInterpolation(_ string: String, _ style: AttrString.Style...) {
var attrs: [NSAttributedString.Key: Any] = [:]
style.forEach { attrs.merge($0.attributes, uniquingKeysWith: {$1}) }
let astr = NSAttributedString(string: string, attributes: attrs)
self.attributedString.append(astr)
}
}

现在可以将多种样式混合起来了!

let str: AttrString = """
Hello \(user, .color(.blue), .underline(.red, .single)), how do you like this?
"""

支持图像

NSAttributedString 的另一种能力是使用 NSAttributedString(attachment: NSTextAttachment) 添加图像,让它成为字符串的一部分。要实现它,仅需要实现 appendInterpolation(image: NSImage) 并调用它。

我希望为这个特性顺便加上缩放图像的能力。由于我是在 macOS 的 playground 上尝试的,它的图形上下文是翻转的,所以也得将图像翻转回来(注意这个细节可能会和 iOS 上实现对 UIImage 的支持时不一样)。这里是我的做法:

extension AttrString.StringInterpolation {
func appendInterpolation(image: NSImage, scale: CGFloat = 1.0) {
let attachment = NSTextAttachment()
let size = NSSize(
width: image.size.width * scale,
height: image.size.height * scale
)
attachment.image = NSImage(size: size, flipped: false, drawingHandler: { (rect: NSRect) -> Bool in
NSGraphicsContext.current?.cgContext.translateBy(x: 0, y: size.height)
NSGraphicsContext.current?.cgContext.scaleBy(x: 1, y: -1)
image.draw(in: rect)
return true
})
self.attributedString.append(NSAttributedString(attachment: attachment))
}
}

样式嵌套

最后,有时候你会希望应用一个样式在一大段文字上,但里面可能也包含了子段落的样式。就像 HTML 里的 "<b>Hello <i>world</i></b>",整段是粗体但包含了一部分斜体的。

现在我们的 API 还不支持这样,所以让我们来加上它。思路是允许将一串 Style… 不止应用在 String 上,还能应用在已经存在属性的 AttrString 上。

这个实现和 appendInterpolation(_ string: String, _ style: Style…) 相似,但会修改 AttrString.attributedString添加属性到上面,而不是单纯用 String 创建一个全新的 NSAttributedString

extension AttrString.StringInterpolation {
func appendInterpolation(wrap string: AttrString, _ style: AttrString.Style...) {
var attrs: [NSAttributedString.Key: Any] = [:]
style.forEach { attrs.merge($0.attributes, uniquingKeysWith: {$1}) }
let mas = NSMutableAttributedString(attributedString: string.attributedString)
let fullRange = NSRange(mas.string.startIndex..<mas.string.endIndex, in: mas.string)
mas.addAttributes(attrs, range: fullRange)
self.attributedString.append(mas)
}
}

上面这些全部完成之后,目标就达成了,终于可以用单纯的字符串加上插值创建一个 AttributedString:

let username = "AliGator"
let str: AttrString = """
Hello \(username, .color(.red)), isn't this \("cool", .color(.blue), .oblique, .underline(.purple, .single))?

\(wrap: """
\(" Merry Xmas! ", .font(.systemFont(ofSize: 36)), .color(.red), .bgColor(.yellow))
\(image: #imageLiteral(resourceName: "santa.jpg"), scale: 0.2)
""", .alignment(.center))

Go there to \("learn more about String Interpolation", .link("https://github.com/apple/swift-evolution/blob/master/proposals/0228-fix-expressiblebystringinterpolation.md"), .underline(.blue, .single))!
"""

imgage

结论

期待你享受这一系列 StringInterpolation 文章,并且能从中瞥到这个新设计威力的冰山一角。

你可以 在这下载我的 Playground 文件,里面有 GitHubComment(见 第一部分),AttrString 的全部实现,说不定还能从我简单实现 RegEX 的尝试中得到一些灵感。

这里还有更多更好的思路去使用 Swift 5 中新的 ExpressibleByStringInterpolation API - 包括 Erica Sadun 博客里这篇这篇这篇 - 还在犹豫什么,阅读更多……从中感受乐趣吧!


  1. 这篇文章和 Playground 里的代码,需要使用 Swift 5。在写作时,最新的 Xcode 版本是 10.1,Swift 4.2,所以如果你想尝试这些代码,需要遵循官方指南去下载开发中的 Swift 5 快照。安装 Swift 5 工具链并在 Xcode 偏好设置里启用并不困难(见官方指南)。
  2. 当然,这里仅作为 Demo,只实现了一部分样式。未来可以延伸思路让 Style 类型支持更多的样式,在理想情况下,可以覆盖所有存在 NSAttributedString.Key

本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 http://swift.gg

Chris Lattner 讲述 Swift 起源故事

作者:Ole Begemann,原文链接,原文日期:2019-02-18
译者:jojotov;校对:numbbbbbWAMaker;定稿:Pancf

新推出的 Swift 社区播客第一集 中,Chris Lattner, Garric Nahapetian, 和 John Sundell 讲述了关于 Swift 起源的故事和 Swift 社区的现状。

本文是我整理出的一些比较有趣的东西(为了能更好地阅读而做了部分修改)。你可以看到我主要引用了 Chris Lattner 的讲话,因为我认为他对于 Swift 是如何被创造出来的描述是最值得保留下去的。但这并不代表 John 和 Garric 所说的东西没那么有趣。你真的应该去完整地收听整集播客——反正所花的时间和阅读本文相差无几。

Swift 社区播客 本身也非常值得关注。作为一个让你可以通过各种方式进行贡献的项目,它非常符合我们的预期(上面提到的三位嘉宾在第一集中谈到了更多细节)。在本文的完成过程中,我的工作主要在 creating the transcript 这个 Issue 上进行,甚至在代码格式部分和编辑机器生成的文本部分收到了许多来自于社区的帮助。在此对所有提供过帮助的人表示感谢!

你可以在 GitHub 上找到 完整的记录副本WebVTT 格式)。所有的播客都是由 CC-BY 4.0 授权。


Swift 的起源

(开始于 16:59)

Crhis Lattner: 关于这件事,我必须从 WWDC 2010 开始讲起。当时我们刚刚上线了 ClangC++ 的支持,非常多的人在这件事上面花费了极其巨大的精力。我对这件事感到非常开心的同时,也有一些烦躁,因为我们做了太多的细节工作。而且你很难不经过思考直接编写 C++ 代码,“天呐,应该有比现在更好的实现方法吧!”

因此,我与一个叫 Bertrand Serlet 的哥们儿进行了许多次讨论。Bertrand 当时是软件团队的老大,同时也是一位出类拔萃的工程师。他是一个令人惊叹的人,并且在语言方面有点极客范。当时他正在推进对 Objective-C 的优化工作。我和他进行了许多次一对一的白板会议。

At the time, Swift was called ‘Shiny’.
在那时,Swift 还叫 ‘Shiny’

Bertrand 负责苹果公司所有的软件项目,因此他基本没什么时间。但他总是会让我在工作结束时顺便拜访一下他,看他是不是有空。他经常会呆到很晚,然后我们会在白板上进行非常认真的讨论。我们会谈论非常非常多的东西:新语言要达成的目标、一些奇怪的细节如类型系统,并且我们最终都会把这些讨论变成一份计划书。因此我为他做了这份计划书并演变成构建一个新语言的想法。那时这个新语言还叫做 “Shiny”,寓意着你正在建造一个 很酷的 新东西。[^1] 当然我也是 Firefly 电视节目的粉丝之一。(译者注:”Shiny” 的梗源自 2002 年美国电视节目 Firefly),意思相当于真实世界中的 “cool”。)

John Sundell: 当时的文件后缀是 .shiny 吗?

Chris Lattner: 确实如此。你知道在那个时候,这还是个很小型的项目。真的就只有我和 Bertrand 在讨论这件事。另外就只有一个同样非常出色的工程师 Dave Zarzycki 参与了早期的一些概念上的讨论。

一开始,我们就自然而然地展开了关于内存管理的讨论。当时,我们都确信一点就是:必须要有一个好的方法来解决或改善内存管理,并且我们需要确保 内存安全。因而,你必须有一个自动内存管理功能。

为了达到自动内存管理的目标,我们有史以来第一次以 Swift 的内部设计讨论为起点,并最终在 Objective-C 中实现了此特性。

首先想到的一个关键功能就是 ARC,同时我们需要让编译器自身支持这个功能,而不是通过运行时来实现。Objective-C 当时使用 libauto 垃圾回收系统,但它有着一大堆问题。因此为了达到自动内存管理的目标,我们有史以来第一次以 Swift 的内部设计讨论为起点,并最终在 Objective-C 中实现了此特性。随后有许多的东西都是这样产生的,包括 ARC 和 modules 甚至 literals 及更多类似的功能,它们的确都是由 Swift 的幕后开发主导的。[^2]

John Sundell: 所以在当时,你脑海里已经有许多关于 Shiny 的特性,最后它们都在 Swift 中实现了。但你曾经说过,“我们并不想一直等待新语言开发完成。我们应该把这些非常吸引人的特性加入到 Objective-C 中。”

在构建一个新的语言时,你必须一直问自己,‘为什么不直接优化现有的语言’,这是幕后构思过程的一部分。

Chris Lattner: 或许现在看来,Swift 的出现是必然的,但如果你从另一个角度思考这个问题,在当时并不是所有人都能认识到这一点,甚至连我也不能确定。Bertrand 从过去到现在都一直非常的棒,他一直给予我们极大的鼓励。而且他总是能在质疑中前进。Bertrand 有点类似科学家,他仅仅只是想通过各种途径寻找真相。的确,当时我们有许多疑虑,但同时也有许多好的想法。包括 Bertrand 在内的很多人一直在推进这项工作。在构建一个新的语言时,你必须一直问自己,‘为什么不直接优化现有的语言’,这是幕后构思过程的一部分。而这个问题的答案是,“很显然,我们应该把现有的优化好”。这也是为什么诸如 ARC 的功能会出现。

但是,在 Swift 中,最需要解决的问题是内存安全。在 Objective-C 中,除非去除 C 相关的东西,不然是不可能达到绝对的内存安全。但去除了 C 的 Objective-C 会失去太多的东西, 而它也不再是 Objective-C 了。

Garric Nahapetian: 没错。因此,把一些 Swift 的特性添加到 Objective-C 中就像是 特洛伊木马 一样,可以让大家更容易地信任 Swift ,因为你已经完成了 Objective-C 方面的工作,是这样吗?

Chris Lattner: (这里面)其实有许多有趣的内部动力。我觉得我们非常专注地优化 Objective-C 及其相关的平台。对于开发 Swift 而言,这是一种降低风险的办法。如果说,“我们要把所有东西都一次性推到重做”,而且不经过任何测试的话,肯定会有巨大的风险。但如果你只把“少部分”的东西单独推倒重做,比如一个全新的内存管理系统,然后对它进行迭代、调试并结合社区的力量进行开发的话,就只会产生有限的风险。不过有一点我要说的是,不管是外部的社区还是苹果内部的社区,貌似都在对我们说,“为什么你要优先考虑这个?我们就好像是概率论中的 随机漫步 一样。为什么你要做这个而不是其他的?”因此,这也成为了一个有趣的动力。


初始团队的成长

(开始于 22:49)

Chris Lattner: 苹果拥有着一支非常强大的工程师队伍。那时有非常多的人一起维护 Objective-C,这其实是有点固执的一件事,但同时这也让我们在动态库、应用和其他类似的东西上拥有了十足的深度和背景。正因如此,那时有许多关于优化 Objective-C 的想法涌现。自从乔布斯离开并创立 NeXT 之后,许多杰出的人物都一直参与这项工作并写下了大量相关的白皮书。因此,Objective-C 背后有一个极其庞大的社区在推动着。

当时,我和 Bertrand 以及 Dave 讨论过一些想法,我也开始着手编写一个编译器的原型。不过结果很显然,我很难靠自己去构建出所有的东西。所以最后的事情也理所当然地发生了——大约在 2011 年四月的时候我们与管理层讨论了关于 Swift 的事情,然后也获得批准去调动一小部分人员。Ted KremenekDoug GregorJohn McCall 以及一些其他的杰出工程师都是在那时调入 Swift 项目的。现在回头看看,其实挺有意思的,因为当时是第一次有一些语言和设计专家对 Swift 做了批判性的评价。他们反馈了很多严厉的批判。虽然他们的本意并不是打击我们,但他们的确说的很对——这个语言当时实在是糟透了。

能让这一切顺利进行有一个关键原因,就是我们有机会拉拢一位泛型领域的世界级专家,以及一支构建过 Clang 编译器的团队。同时这支团队也参与过许多不同的有意思的项目,并能够尽可能地发挥他们的工程天赋。虽然他们只是帮助推进和开发构建的一部分人员,但却至关重要。

John Sundell: 在那时 Swift 语言的状况是怎样的?比如,语言的语法是什么样子?编译器的哪部分基础设施已经搭建完成了?它是不是已经接近于原型的阶段甚至更进一步呢?

Chris Lattner: 那时 Swift 已经非常接近原型阶段了。这些资料都是完全公开的,因为在 GitHub 上,变更历史是完全公开 的。变更日志 虽然不能完全追溯所有的历史变更,但已经足够了。

在 Doug 加入之前,Swift 并没有泛型系统。当时我们很想做一个泛型系统,但我不是很有自信能独自设计出来,Doug 却做到了。我记得在很早的时候,John 曾经接手过一个项目,让一个类似语法分析器的东西变得可以真正生成代码。Doug 所做的事情大概就是这样。

有很多零碎的事情我不太记得了,但有些东西我却一直记忆犹新。我记得 varfunc 就是从最初就制定好的。早期的 Swift 中很多基本语法都和现在的 Swift 语法非常接近。

当你构建一样新东西的时候,通常想法是领先于文档的,而文档又先于代码。我们当时情况也非常类似。到现在,想法已经领先于代码非常之多了,这都是无可避免的。

当时的 Swift 已经十分接近于一个原型了。但仍有许许多多的想法未实现。当你构建一样新东西的时候,通常想法是先于文档的,而文档又是先于代码。我们当时情况也非常类似。到现在,想法已经领先于代码非常之多了,这都是无可避免的。


关于 Craig Federighi

(开始于 26:10)

Chris Lattner: 对于社区非常重要的另一位伙伴名叫 Craig Federighi。Craig 在苹果社区中非常出名。在 2011 年早些时候,他加入了这个项目。那时正好 Bertrand 从苹果退休,Craig 便接手了他的工作。

说到 Craig,他是一个非常、非常有趣的人。无论在台上还是台下,他都着非凡的个人魅力。大多数人们都不了解他到底有多么的聪明。他还在许多方面都有着极其深入的研究。我完全没想到,他在语言方面懂得很多。他曾为 Groovy 和许多其他类型的语言作为正式参与者工作过,有些语言我甚至还没接触过。而且,他并不像是一位只会谈论策略的高层人员。他同样关心许多细节的事情,例如闭包语法、关键字和其他东西。

Craig 真的是一位非常严格的任务推动者,同时他也推动着 Swift 的实现以及 Swift 与 Objective-C 的联动。不仅如此,他还关心着 Objective-C 本身的维护;关心着 API;关心着 Objective-C 的 API 导入到 Swift 后的形态,还有相关的一切。Craig 在积极给予反馈的同时,一直保持着在团队和项目上出乎意料的卓越态度。他的帮助对 Swift 的现状影响很大。

Garric Nahapetian: 这真的很酷,因为他刚好是在 WWDC 2014 的讲台上第一位发表演讲的人。

Chris Lattner: 没错。

John Sundell: 然后他就介绍你出场了,对吧?我还记得那句经典的标语:“没有 C 的 Objective-C”。

我对那句“没有 C 的 Objective-C”标语有着复杂的感觉,因为其实我想表达的并不是这样。

Chris Lattner: 说实话,我对那句标语有着复杂的感觉,因为其实我想表达的并不是这样。

John Sundell: 那是句很棒的标语。

Chris Lattner: 在那个时候,以这种方式向社区宣传是很正确的选择。

(……)

在项目的一开始,我的目标其实是构建一个全栈系统。

Chris Lattner: 至于说为什么我当时很矛盾,因为在项目的一开始,我的目标其实是构建一个全栈系统——分析市面上现有的系统,取其精华,弃其糟粕。而且当时的目标是构建一个可以编写固件、编写脚本、编写移动应用和服务端应用甚至是更底层系统代码的语言,同时在语言层面上并非只是通过随意堆砌来实现,我要让它在上面说的所有方面都能表现出色。

所以在当时而言,这个方向绝对是正确的。虽然目前 Swift 还没能达到预期,但欣慰的是它的发展将会使它能够在未来拥有这些的能力。


文档的重要性

(开始于 30:32)

Chris Lattner: (在这里)我还要最后提到一支团队。你目前所看到的 Swift,它是一个编译器,是一门语言,是一系列 API 的集合,也是一个 IDE。但让这些事情能够成真,并让 Swift 走向大众,离不开开发者发布团队的工作。他们是 Apple 的科技编辑,负责编写如 Swift 编程语言书籍 之类的东西。Swift 的成功和快速适应市场离不开当时第一时间发布的高质量文档和书籍。直到今天,他们仍在维护这些文档,真的非常不可思议。

我们直接把这些科技编辑拉入了设计讨论会

我记得当时我们直接把这些科技编辑拉入了设计讨论会。像 Tim Isted、Brian Lanier 和 Alex Martini 这些人,他们在周会上花了大量的时间争论着一些细节问题——“我们是否应该使用点语法?”“我们应该使用这个关键字还是那个关键字?”或者是“我们是应该把 func 改为 def 吗?”同时还讨论着——类型系统的深度以及 代码生成) 算法应如何工作;我们如何达到较好的性能?字符串 应如何运作?还有各种各样的问题。

如果你能在设计阶段就考虑到如何像大家解释这门语言的话,工作会进行得更顺利。

我见过很多次这种事情,当你构建完一个系统后你还要尝试着解释它。然后当你开始解释这个系统时,真的会陷入一个尴尬的处境,比如:“天啊,我竟然还要去解释这个东西是如何工作的”。如果你可以在设计阶段就处理好反馈信息,并引入文档,引入这些关于如何向大家解释的工作,你会进行得更顺利。


Swift 的开发工作是一个团队效应

(开始于 34:58)

Chris Lattner: 我想说的是,很多人会说“Chris 发明了 Swift”。虽然我的确在很多年的时间里一直用不同的方法推动着 Swift 的开发,也算是带领着整个项目,但事实上其实他们都忽略了一点,有数以百计的人为 Swift 的许多关键问题作出了贡献。例如构建调试器、构建 IDE、构建 Playgrounds、构建教育相关的内容、构建各种东西以及相关的社区。

我在许多地方都不如他们优秀。他们几乎组成了一个不仅是在 Apple 内部,同时活跃于 Apple 之外的社区,并一起推动着 Swift 相关功能的构建,并以他们自己的方式做着贡献。这就是为什么 Swift 能发展的如今的规模,我认为这也是 Swift 能一直成长下去的原因。

Garric Nahapetian: 这也是我们录制这集播客的原因之一——让这些在幕后做过贡献的人可以站在聚光灯下。


不要像编译器开发者一样写 Swift

(开始于 42:15)

Chris Lattner: 有件事我觉得很神奇,你们两个写的 Swift 代码比我写的还多。我可能对 Swift 的内部实现、它为什么会是这样的、它是如何做到某些事的,以及它是如何运作的比较了解,但你们却拥有着真正以 Swift 来构建产品的经验

John Sundell: 是的,这很有趣。有许多许多人在为 Swift 项目工作,为它的编译器工作,而且他们大部分时间都在写 C++。我想顺便问一下,这种感觉是怎样的?你设计了这门非常酷的语言,大家也逐渐开始使用它了,但你却还在使用 C++。

Chris Lattner: (大笑) 这让我痛不欲生。这真的太可怕了。就像老天开的玩笑一样,强制我一直在写 C++。


关于社区反馈

(开始于 43:11)

Chris Lattner: 为什么 Swift 现在能发展得这么好,拥有一个庞大的的社区,而且大家都习惯于在上面写博客,这肯定是都主要原因之一对吧?社区的反馈的确影响了 Swift 1 和 2 。那些抱怨就像是提醒我们的信号一样,“这根本不合理”,“我在这上面遇到了问题,还有那个和另外的都遇到了问题”。这些的确帮了我们很多,特别是在优化、排期和推动最终的 Swift 版本方面。

早期的社区反馈的确影响了 Swift 1 和 2。

比较意外,也可以说是我们有意为之的是,在 Swift 1 中没有加入错误处理机制。同时我们也没有加入协议扩展,这些能力我们绝对是希望 Swift 能够拥有的,但只是错过了发布日程。因此我们很清楚必须要去构建这些功能,但在第一两年间这些东西却是直接由社区最终推动完成的。然后当 Swift 开源Swift Evolution 变成了一个伟大的项目。这个项目可能在人们的时间效率优化方面表现并不理想,但它却是让 Swift 非比寻常的重要因素。我想这项荣誉属于所有在社区中花时间为 Swift 的功能优化和工作推进提供帮助的人。

John Sundell: 我所想到的就是,不仅是那些文章和内容,同时包括开源在内的所有东西,都是如何大量反哺了 Swift 自身。例如,类似 Codable 功能,在它未完成之前人们会开发数以千计的 JSON 映射库。我也是其中之一。我曾经构建了 Unbox ,因为在 Objective-C 中你不需要在这方面花费许多时间和精力。你只需要说,这是个字典,那我们就访问它的某个 Key 然后假设返回结果永远是字符串。但一旦你坐下来把同样的代码以 Swift 实现的话,你就会发现需要用到大量的 if let。因此我能想象到这些大家都尝试解决的东西,绝对会以不同的方式反哺 Swift 自身的设计进程。

Chris Lattner: 是的,这无可厚非。而且 Codable 的设计来自于 Apple 的一支非 Swift 核心开发团队的动态库团队。他们对这个工作真的很有耐心,他们实现并推动着它,并且一直支持着它上线。

很难想象到底社区影响了 Swift 多少。

社区呈现了各种各样的东西对吧?比如 Result。为什么 Result 要加入到 Swift 5 中?因为我们一次又一次地构造了它。即使 核心团队 不希望有一个 Result 类型,因为这仿佛是语言的失败体现;即使我们一致认为 Result 类型不是必要的。但社区中却一直有清晰且强烈的声音,“你们看,我们需要它。不管在长期看来它是否理想,但我们现在的确需要。”因此社区真的影响了很多东西。而且你可能难以想象到底社区影响了 Swift 多少。


关于初始预期

(开始于 46:07)

John Sundell: 对于 Swift 的现状,以及你发布 Swift 后它的改变,这些是否都符合你的预期呢?经过了这些年,对于你在最初阶段的想法,Swift 现在有多少是符合的?

当 Swift 1 发布时,我们也有一些疑问,我们是否可以在 Objective-C 的社区中获得一席之地?我们是否能在 iOS 的生态系统中获得一席之地,或者是将会分成几派?

Chris Lattner: 这么说吧,我觉得预期也会随时间改变。如果回到 2010-2011 年,我并没有预想着它会有多成功。我承认当时我只是觉得这是个有趣的业余项目。这原本就是个填补夜晚和周末时间的东西。你知道,在整日的工作后再去做一个业余项目是很有趣也很具挑战性的。当它越来越接近完成,并直到 Swift 1 发布时,我们都一些疑问——我们是否可以在 Objective-C 的社区中获得一席之地?我们是否能在 iOS 的生态系统中获得一席之地,或者是将会分成几派?我们当时的确有这样的疑虑。现在我可以说我会感到很欣慰,因为我觉得社区中绝大多数人都对 Swift 持乐观态度。

Chris Lattner: 不过现在依然有新的领域等待探索。Swift 在服务端的表现一直越来越好,但仍有大量的工作需要完成。在外部,也有许多不同的社区活跃着。我对数字和 机器学习的社区 特别感兴趣,这些对全世界都有重要的意义。在这些社区中,有着许多有趣的人,我觉得 Swift 在这里可以发展得很好。

Swift 的全球制霸只是一个开玩笑的目标,但它来源于使用和热爱 Swift 的人们的信念。

我有时会开玩笑地说,我们的目标是 Swift 全球制霸。这只是一个玩笑目标,但它来源于使用和热爱 Swift 的人们的信念。如果真是这样的话,我会非常愿意把这份愉悦带给人们,并帮助改善整个世界。现实中仍有许多让人头痛的系统对吧?仍有许多人还在写着 C 语言代码。就那些 bug 和安全脆弱性 来说,真是让人感觉十分不幸。同时,我们还要面对生态系统的问题,以及许多其他的挑战要征服,这都是我们作为一个社区可以完成的。在这方面,我认为有着无限的可能。


在 Apple 社区以外推广 Swift

(开始于 50:18)

Chris Lattner: 尽管人们现在都是以积极、乐观的态度来讨论 Swift,可 Swift 仍有许多问题。我觉得我们必须保持开放的态度来讨论这个,并把它看作一个解决问题的练习。主要的问题集中在 Linux 生态系统上面,与之相比,Windows 生态系统 的问题简直不值一提。为了让 Swift 的受众更广泛,我们真的还有许多工作要做。

Swift 仍有许多问题。主要的问题集中在 Linux 生态系统上面。Swift 在 Windows 生态系统 的问题简直不值一提。

Chris Lattner: 我们的目标是打造一个不排外的社区。可事与愿违的是,如果你不是一名 Apple 开发者,你可能会在搜索 Swift 相关的东西时感到融入不了社区,因为搜索结果都是一些 iOS 相关的讨论。

如果你不是一名 Apple 开发者,你可能会在搜索 Swift 相关的东西时感到融入不了社区,因为搜索结果都是一些 iOS 相关的讨论。

John Sundell: 完全正确。比如“这是如何在 UIViewController 中完成它的方法。”

Chris Lattner: 没错。这会让你觉得自己是个外来者,但这并不是我们的初衷。我认为这不是有意为之的,但它却是真实存在的。这正是我们社区所面对的一大挑战。我目前还不知道有没有比较完美解决方案,不过我想我们总会找到的。


关于 Swift 的演进

(开始于 55:12)

Chris Lattner: (关于 Swift 的演进)这是个非常复杂的事情,我大概要好几个小时才能全部讲完。如果简短地说一下我的想法——开放要好于封闭。如果你能让更多人参与,那么你肯定能得到一个更好的结果。我想 Swift Evolution) 的项目进展有很多问题,Garric 也列举了一些。但这不妨碍它是一个很好的东西。同时有一点我认为是有益的,就是它很好地减缓了语言的演进。因为小心谨慎地发展一个语言总比过快地发展好。

我觉得强制地推动一系列文档进展也是一件很好的事情,因为文档是十分重要的。Swift Evolution 项目引领了苹果与社区在某种规范下,以不同的方式进行合作,这不仅有趣,也是非常棒的一件事。

我不认为 Swift Evolution 项目是一成不变的。在这几年的时间里,它在不同方面都发生了改变。同时,我们一直在艰难地权衡着它的目的——到底是为社区输出设计规范还是帮助社区确定优先级?对我们来说,这是个具有挑战性的问题,因为当你把它完全交给社区去执行时,你可能会失去一些细节的东西。但在一些大方向上,整个 Swift 的世界都是认同的——比如 并发性

这是我们的一次非常大的尝试,而且通过一个自底向上的社区流程来实现它不是一件容易的事。因此很多事对我来说都是未知的。我觉得 Swift Evolution 是一个很棒的项目。我也很开心我们现在拥有这个项目。尽管我同意它不是我们所唯一拥有的,也不应该是唯一的,但我仍觉得它绝对是业界标杆之一。

(……)

我一直在寻找 Swift 包管理生态系统的催化剂,到底应该以服务端 Swift 的社区为起点,还是以机器学习社区为起点。

从某种意义上来说,Swift 的每一个变动,都需要一段时间来消化。当一个大的新能力出现时,社区都需要一段时间来弄明白它、应用它、找出它的使用场景和它如何与其他功能兼容。因此,花费的这些时间都是值得的。我从 Swift Evolution 所认识到的最重要的事,就是社区的合理推动所带来的力量。Swift Evolution 真的为我们聚集了许多来自社区的语言极客,让他们同时关注 Swift 项目一个特定方面。我一直在寻找 Swift 包管理生态系统发展的催化剂,到底应该以服务端 Swift 的社区为起点,还是把机器学习社区聚集起来干点大事。

我们如何去寻找这种催化剂——让大家在合适的地方聚在一起,通过他们自己的力量构建一个项目,发挥他们的能力和天赋,让大家协作工作?

[^1]: Jordan Rose 分享了一则与 Shiny 这个名字相关的佚事 :.swiftmodule 文件中的“魔法数字”是 E2 9C A8 0E。它的前三个字节是 ✨ 的 UTF-8 字符(U+2728 SPARKLES)。

[^2]: Greg Parker 声称 ARC 只是间接地从 Swift 的开发过程中产生:ARC 的实现早于 Swift 的实现。但在幕后的工作中,Swift 早期的设想的确推进了管理层提供必要的资源去构建和部署 ObjC 的 ARC。

本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 http://swift.gg

图像优化

作者:Jordan Morgan,原文链接,原文日期:2018-12-11
译者:Nemocdz;校对:numbbbbbWAMaker;定稿:Pancf


俗话说得好,最好的相机是你身边的那个。那么毫无疑问 - iPhone 可以说是这个星球最重要的的相机。而这在业界也已经达成共识。

在度假?不偷偷拍几张记录在你的 Instagram 故事里?不存在的。

出现爆炸新闻?查看 Twitter,就可以知道是哪些媒体正在报道,通过他们揭露事件的实时照片。

等等……

正因为图像在平台上无处不在,如果管理不当,很容易出现性能和内存问题。稍微了解下 UIKit,搞清楚它处理图像的机制,可以节省大量时间,避免做无用功。

理论知识

快问快答 - 这是一张我漂亮(且时髦)女儿的照片,大小为 266KB,在一个 iOS 应用中展示它需要多少内存?

剧透警告 - 答案不是 266KB,也不是 2.66MB,而是接近 14MB。

为啥呢?

iOS 实际上是从一幅图像的尺寸计算它占用的内存 - 实际的文件大小会比这小很多。这张照片的尺寸是 1718 像素宽和 2048 像素高。假设每个像素会消耗我们 4 个比特:

1718 * 2048 * 4 / 1000000 = 14.07 MB 占用

假设你有一个用户列表 table view,并且在每一行左边使用常见的圆角头像来展示他们的照片。如果你认为这些图像会像洁食(犹太人的食品,比喻事情完美无瑕)一样,每个都被类似 ImageOptim 的工具压缩过,那可就大错特错了。即使每个头像的大小只有 256x256,也会占用相当一部分内存。

渲染流程

综上所述 - 了解幕后原理是值得的。当你加载一张图片时,会执行以下三个步骤:

1)加载 - iOS 获取压缩的图像并加载到 266KB 的内存(在我们这个例子中)。这一步没啥问题。

2)解码 - 这时,iOS 获取图像并转换成 GPU 能读取和理解的方式。这里会解压图片,像上面提到那样占用 14MB。

3)渲染 - 顾名思义,图像数据已经准备好以任意方式渲染。即使只是在一个 60x60pt 的 image view 中。

解码阶段是消耗最大的。在这个阶段,iOS 会创建一块缓冲区 - 具体来说是一块图像缓冲区,也就是图像在内存中的表示。这解释了为啥内存占用大小和图像尺寸有关,而不是文件大小。因此也可以理解,为什么在处理图片时,尺寸如此重要。

具体到 UIImage,当我们传入从网络或者其它来源读取的图像数据时,它会将数据解码到缓冲区,但不会考虑数据的编码方式(比如 PNG 或者 JPG)。然而,缓冲区实际上会保存到 UIImage 中。由于渲染不是一瞬间的操作,UIImage 会执行一次解码操作,然后一直保留图像缓冲区。

接着往下说 - 任何 iOS 应用中都有一整块的帧缓冲区。它会保存内容的渲染结果,也就是你在屏幕上看到的东西。每个 iOS 设备负责显示的硬件都用这里面单个像素信息逐个点亮物理屏幕上合适的像素点。

处理速度非常重要。为了达到黄油般顺滑的每秒 60 帧滑动,在信息发生变化时(比如给一个 image view 赋值一幅图像),帧缓冲区需要让 UIKit 渲染 app 的 window 以及它里面所有层级的子视图。一旦延迟,就会丢帧。

觉得 1/60 秒太短不够用?Pro Motion 设备已经将上限拉到了 1/120 秒。

尺寸正是问题所在

我们可以很简单地将这个过程和内存的消耗可视化。我创建了一个简单的应用,可以在一个 image view 上展示需要的图像,这里用的是我女儿的照片:

let filePath = Bundle.main.path(forResource:"baylor", ofType: "jpg")!
let url = NSURL(fileURLWithPath: filePath)
let fileImage = UIImage(contentsOfFile: filePath)

// Image view
let imageView = UIImageView(image: fileImage)
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFit
imageView.widthAnchor.constraint(equalToConstant: 300).isActive = true
imageView.heightAnchor.constraint(equalToConstant: 400).isActive = true

view.addSubview(imageView)
imageView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
imageView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true

实践中请注意强制解包。这里只是一个简单的场景。

完成之后就会是这个样子:

虽然展示图片的 image view 尺寸很小,但是用 LLDB 就可以看到图像的真正尺寸。

<UIImage: 0x600003d41a40>, {1718, 2048}

需要注意的是 - 这里的单位是。所以当我在 3x 或 2x 设备时,可能还需要额外乘上这个数字。我们可以用 vmmap 来确认这张图像是否占用了 14 MB:

vmmap --summary baylor.memgraph

一部分输出(省略一些内容以便展示):

Physical footprint:         69.5M
Physical footprint (peak): 69.7M

我们看到这个数字接近 70MB,这可以作为基准来确认针对性优化的成果。如果我们用 grep 命令查找 Image IO,或许会看到一部分图像消耗:

vmmap --summary baylor.memgraph | grep "Image IO"

Image IO 13.4M 13.4M 13.4M 0K 0K 0K 0K 2

啊哈 - 这里有大约 14MB 的脏内存,和我们前面的估算一致。如果你不清楚每一列表示什么,可以看下面这个截图:

通过这个例子可以清楚地看到,哪怕展示在 300x400 image view 中,图像也需要完整的内存消耗。图像尺寸很重要,但是尺寸并不是唯一的问题。

色彩空间

能确定的是,有一部分内存消耗来源于另一个重要因素 - 色彩空间。在上面的例子中,我们的计算基于以下假设 - 图像使用 sRGB 格式,但大部分 iPhone 不符合这种情况。sRGB 每个像素有 4 个字节,分别表示红、蓝、绿、透明度。

如果你用支持宽色域的设备进行拍摄(比如 iPhone 8+ 或 iPhone X),那么内存消耗将变成两倍,反之亦然。Metal 会用仅有一个 8 位透明通道的 Alpha 8 格式。

这里有很多可以把控和值得思考的地方。这也是为什么你应该用 UIGraphicsImageRenderer 代替 UIGraphicsBeginImageContextWithOptions 的原因之一。后者总是会使用 sRGB,因此无法使用宽色域,也无法在不需要的时候节省空间。在 iOS 12 中,UIGraphicsImageRenderer 会为你做正确的选择。

不要忘了,很多图像并不是真正的摄影作品,只是一些绘图操作。如果你错过了我最近的文章,可以再阅读一遍下面的内容:

let circleSize = CGSize(width: 60, height: 60)

UIGraphicsBeginImageContextWithOptions(circleSize, true, 0)

// Draw a circle
let ctx = UIGraphicsGetCurrentContext()!
UIColor.red.setFill()
ctx.setFillColor(UIColor.red.cgColor)
ctx.addEllipse(in: CGRect(x: 0, y: 0, width: circleSize.width, height: circleSize.height))
ctx.drawPath(using: .fill)

let circleImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()

上面的圆形图像用的是每个像素 4 个字节的格式。如果换用 UIGraphicsImageRenderer,通过渲染器自动选择正确的格式,让每个像素使用 1 个字节,可以节省高达 75% 的内存:

let circleSize = CGSize(width: 60, height: 60)
let renderer = UIGraphicsImageRenderer(bounds: CGRect(x: 0, y: 0, width: circleSize.width, height: circleSize.height))

let circleImage = renderer.image{ ctx in
UIColor.red.setFill()
ctx.cgContext.setFillColor(UIColor.red.cgColor)
ctx.cgContext.addEllipse(in: CGRect(x: 0, y: 0, width: circleSize.width, height: circleSize.height))
ctx.cgContext.drawPath(using: .fill)
}

缩小图片 vs 向下采样

现在我们从简单的绘图场景回到现实世界 - 许多图片其实并不是艺术作品,只是自拍或者风景照。

因此有些人可能会假设(并且确实相信)通过 UIImage 简单地缩小图片就够了。但我们前面已经解释过,缩小尺寸并不管用。而且根据 Apple 工程师 kyle Howarth 的说法,由于内部坐标转换的原因,缩小图片的优化效果并不太好。

UIImage 导致性能问题的根本原因,我们在渲染流程里已经讲过,它会解压原始图像到内存中。理想情况下,我们需要一个方法来减少图像缓冲区的尺寸。

庆幸的是,我们可以修改图像尺寸,来减少内存占用。很多人以为图像会自动执行这类优化,但实际上并没有。

让我们尝试用底层的 API 来对它进行向下采样:

let imageSource = CGImageSourceCreateWithURL(url, nil)!
let options: [NSString:Any] = [kCGImageSourceThumbnailMaxPixelSize:400,
kCGImageSourceCreateThumbnailFromImageAlways:true]

if let scaledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary) {
let imageView = UIImageView(image: UIImage(cgImage: scaledImage))

imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFit
imageView.widthAnchor.constraint(equalToConstant: 300).isActive = true
imageView.heightAnchor.constraint(equalToConstant: 400).isActive = true

view.addSubview(imageView)
imageView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
imageView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
}

通过这种取巧的展示方法,会获得和以前完全相同的结果。不过在这里,我们使用了 CGImageSourceCreateThumbnailAtIndex(),而不是直接将原始图片放进 image view。再次使用 vmmap 来确认优化是否有回报(同样,省略部分内容以便展示):

vmmap -summary baylorOptimized.memgraph

Physical footprint: 56.3M
Physical footprint (peak): 56.7M

效果很明显。之前是 69.5M,现在是 56.3M,节省了 13.2M。这个节省相当大,几乎和图片本身一样大。

更进一步,你可以在自己的案例中尝试更多可能的选项来进行优化。在 WWDC 18 的 Session 219,“Images and Graphics Best Practices“中,苹果工程师 Kyle Sluder 展示了一种有趣的方式,通过 kCGImageSourceShouldCacheImmediately 标志位来控制解码时机,:

func downsampleImage(at URL:NSURL, maxSize:Float) -> UIImage
{
let sourceOptions = [kCGImageSourceShouldCache:false] as CFDictionary
let source = CGImageSourceCreateWithURL(URL as CFURL, sourceOptions)!
let downsampleOptions = [kCGImageSourceCreateThumbnailFromImageAlways:true,
kCGImageSourceThumbnailMaxPixelSize:maxSize
kCGImageSourceShouldCacheImmediately:true,
kCGImageSourceCreateThumbnailWithTransform:true,
] as CFDictionary

let downsampledImage = CGImageSourceCreateThumbnailAtIndex(source, 0, downsampleOptions)!

return UIImage(cgImage: downsampledImage)
}

这里 Core Graphics 不会开始图片解码,直到你请求缩略图。另外要注意的是,两个例子都传入了 kCGImageSourceCreateThumbnailMaxPixelSize,如果不这样做,就会获得和原图同样尺寸的缩略图。根据文档所示:

“…如果没指定最大尺寸,返回的缩略图将会是完整图像的尺寸,这可能并不是你想要的。”

所以上面发生了什么?简而言之,我们将缩放的结果放入缩略图中,从而创建的是比之前小很多的图像解码缓冲区。回顾之前提到的渲染流程,在第一个环节(加载)中,我们给 UIImage 传入的缓冲区是需要绘制的图片尺寸,不是图片的真实尺寸。

如何用一句话总结本文?想办法对图像进行向下采样,而不是使用 UIImage 去缩小尺寸。

附赠内容

除了向下采样,我自己还经常使用 iOS 11 引入的 预加载 API。请记住,我们是在解码图像,哪怕是放在 Cell 展示之前执行,也会消耗大量 CPU 资源。

如果应用持续耗电,iOS 可以优化电量消耗。但是我们做的向下采样一般不会持续执行,所以最好在一个队列中执行采样操作。与此同时,你的解码过程也实现了后台执行,一石多鸟。

做好准备,下面即将为您呈现的是——我自己业余项目里的 Objective-C 代码示例:

// 不要用全局异步队列,使用你自己的队列,从而避免潜在的线程爆炸问题
- (void)tableView:(UITableView *)tableView prefetchRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths
{
if (self.downsampledImage != nil ||
self.listItem.mediaAssetData == nil) return;

NSIndexPath *mediaIndexPath = [NSIndexPath indexPathForRow:0
inSection:SECTION_MEDIA];
if ([indexPaths containsObject:mediaIndexPath])
{
CGFloat scale = tableView.traitCollection.displayScale;
CGFloat maxPixelSize = (tableView.width - SSSpacingJumboMargin) * scale;

dispatch_async(self.downsampleQueue, ^{
// Downsample
self.downsampledImage = [UIImage downsampledImageFromData:self.listItem.mediaAssetData
scale:scale
maxPixelSize:maxPixelSize];

dispatch_async(dispatch_get_main_queue(), ^ {
self.listItem.downsampledMediaImage = self.downsampledImage;
});
});
}
}

建议使用 asset catalog 来管理原始图像资源,它已经实现了缓冲区优化(以及更多功能)。

想成为内存和图像处理专家?不要错过 WWDC 18 这些信息量巨大的 session:

总结

学无止境。如果选择了编程,你就必须每小时跑一万英里才能跟得上这个领域创新和变化的步伐……换句话说,一定会有很多你根本不知道的 API、框架、模式或者优化技巧。

在图像领域也是如此。大多数时候,你初始化一个了大小合适的 UIImageView 就不管了。我当然知道摩尔定律。现在手机确实很快,内存也很大,但是你要知道 - 将人类送上月球的计算机只有不到 100KB 内存。

长期和魔鬼共舞(译者注:比喻不管内存问题),它总有露出獠牙的那天。等到一张自拍就占掉 1G 内存的时候,后悔也来不及了。希望上述的知识和技术能帮你节省一些 debug 时间。

下次再见 ✌️。

本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 http://swift.gg

宏定义与可选括号

作者:Mike Ash,原文链接,原文日期:2015-03-20
译者:俊东;校对:numbbbbbNemocdz;定稿:Pancf

前几天我遇到了一个有趣的问题:如何编写一个 C 语言预处理器的宏,删除包围实参的括号?

今天的文章,将为大家分享我的解决方案。

起源

C 语言预处理器是一个相当盲目的文本替换引擎,它并不理解 C 代码,更不用说 Objective-C 了。它的工作原理还算不错,可以应付大部分情况,但偶尔也会出现判断失误。

这里举个典型的例子:

XCTAssertEqualObjects(someArray, @[ @"one", @"two" ], @"Array is not as expected");

这会无法编译,并且会出现非常古怪的错误提示。预处理器查找分隔宏参数的逗号时,没能将数组结构 @ [...] 中的东西理解为一个单一的元素。结果代码尝试比较 someArray@[@"one"。断言失败消息 @"two"]@"Array is not as expected" 是另外的实参。这些半成品部分用于 XCTAssertEqualObjects 的宏扩展中,生成的代码当然错得离谱。

要解决这个问题也很容易:添加括号就行。预编译器不能识别 [],但它确实知道 () 并且能够理解应该忽略里面的逗号。下面的代码就能正常运行:

XCTAssertEqualObjects(someArray, (@[ @"one", @"two" ]), @"Array is not as expected");

在 C 语言的许多场景下,你添加多余的括号也不会有任何区别。宏扩展开之后,生成的代码虽然在数组文字周围有括号,但没有异常。你可以写搞笑的多层括号表达式,编译器会愉快地帮你解析到最里面一层:

NSLog(@"%d",((((((((((42)))))))))));

甚至将 NSLog 这样处理也行:

((((((((((NSLog))))))))))(@"%d",42);

在 C 中有一个地方你不能随意添加括号:类型(types)。例如:

int f(void); // 合法
(int) f(void); // 不合法

什么时候会发生这种情况呢?这种情况并不常见,但如果你有一个使用类型的宏,并且类型包含的逗号不在括号内,则会出现这种情况。宏可以做很多事情,当一个类型遵循多个协议时,在 Objective-C 中可能出现一些类型带有未加括号的逗号;当使用带有多个模板参数的模板化类型时,在 C++ 中也可能出现。举个例子,这有一个简单的宏,创建从字典中提供静态类型值的 getter

#define GETTER(type,name) \
- (type)name { \
return [_dictionary objectForKey: @#name]; \
}

你能这样使用它:

@implementation SomeClass {
NSDictionary *_dictionary;
}

GETTER(NSView *,view)
GETTER(NSString *,name)
GETTER(id<NSCopying>,someCopyableThing)

到目前为止没问题。现在假设我们想要创建一个遵循两个协议的类型:

GETTER(id<NSCopying,NSCoding>,someCopyableAndCodeableThing)

哎呀!宏不起作用了。而且添加括号也无济于事:

GETTER((id<NSCopying,NSCoding>),someCopyableAndCodeableThing)

这会产生非法代码。这时我们需要一个删除可选括号的 UNPAREN 宏。将 GETTER 宏重写:

#define GETTER(type,name) \
- (UNPAREN(type))name { \
return [_dictionary objectForKey: @#name]; \
}

我们该怎么做呢?

必须的括号

删除括号很容易:

#define UNPAREN(...) __VA_ARGS__
#define GETTER(type,name) \
- (UNPAREN type)name { \
return [_dictionary objectForKey: @#name]; \
}

虽然看上去很扯,但这的确能运行。预编译器将 type 扩展为 (id <NSCopying,NSCoding>),生成 UNPAREN (id<NSCopying, NSCoding>)。然后它会将 UNPAREN 宏扩展为 id <NSCopying,NSCoding>。括号,消失!

但是,之前使用的 GETTER 失败了。例如,GETTER(NSView *,view) 在宏扩展中生成 UNPAREN NSView *。不会进一步扩展就直接提供给编译器。结果自然会报编译器错误,因为 UNPAREN NSView * 是无法编译的。这虽然可以通过编写 GETTER((NSView *),view) 来解决,但是被迫添加这些括号很烦人。这样的结果可不是我们想要的。

宏不能被重载

我立刻想到了如何摆脱剩余的 UNPAREN。当你想要一个标识符消失时,你可以使用一个空的 #define,如下所示:

#define UNPAREN

有了这个,a UNPAREN b 的序列变为 a b。完美解决问题!但是,如果已经存在带参数的另一个定义,则预处理器会拒绝此操作。即使预处理器可能选择其中一个,它也不会同时存在两种形式。如果可行的话,这能有效解决我们的问题,但可惜的是并不允许:

#define UNPAREN(...) __VA_ARGS__
#define UNPAREN
#define GETTER(type,name) \
- (UNPAREN type)name { \
return [_dictionary objectForKey: @#name]; \
}

这无法通过预处理器,它会由于 UNPAREN 的重复 #define 而报错。不过,它引导我们走上了成功的道路。现在的瓶颈是怎么找出一种方法来实现相同的效果,而不会使两个宏具有相同的名称。

关键

最终目标是让 UNPAREN(x)UNPAREN((x)) 结果都是 x。朝着这个目标迈出的第一步是制作一些宏,其中传递 x(x) 产生相同的输出,即使它并不确定 x 是什么。这可以通过将宏名称放在宏扩展中来实现,如下所示:

#define EXTRACT(...) EXTRACT __VA_ARGS__

现在如果你写 EXTRACT(x),结果是 EXTRACT x。当然,如果你写 EXTRACT x,结果也是 EXTRACT x,就像没有宏扩展的情况。这仍然给我们留下一个 EXTRACT。虽然不能用 #define 直接解决,但这已经进步了。

标识符粘合

预处理器有一个操作符 ##,它将两个标识符粘合在一起。例如,a ## b 变为 ab。这可以用于从片段构造标识符,但也可以用于调用宏。例如:

#define AA 1
#define AB 2
#define A(x) A ## x

从这里可以看到,A(A) 产生 1A(B) 产生 2

让我们将这个运算符与上面的 EXTRACT 宏结合起来,尝试生成一个 UNPAREN 宏。由于 EXTRACT(...) 使用前缀 EXTRACT 生成实参,因此我们可以使用标识符粘合来生成以 EXTRACT 结尾的其他标记。如果我们 #define 那个新标记为空,那就搞定了。

这是一个以 EXTRACT 结尾的宏,它不会产生任何结果:

#define NOTHING_EXTRACT

这是对 UNPAREN 宏的尝试,它将所有内容放在一起:

#define UNPAREN(x) NOTHING_ ## EXTRACT x

不幸的是,这并不能实现我们的目标。问题在操作顺序上。如果我们写 UNPAREN((int)),我们将会得到:

UNPAREN((int))
NOTHING_ ## EXTRACT (int)
NOTHING_EXTRACT (int)
(int)

标示符粘合太早起作用,EXTRACT 宏永远不会有机会扩展开。

可以使用间接的方式强制预处理器用不同的顺序判断事件。我们可以制作一个 PASTE 宏,而不是直接使用 ##

#define PASTE(x,...) x ## __VA_ARGS__

然后我们将根据它编写 UNPAREN

#define UNPAREN(x)  PASTE(NOTHING_,EXTRACT x)

仍然不起作用。情况如下:

UNPAREN((int))
PASTE(NOTHING_,EXTRACT (int))
NOTHING_ ## EXTRACT (int)
NOTHING_EXTRACT (int)
(int)

但更接近我们的目标了。序列 EXTRACT(int) 显然没有触发标示符粘合操作符。我们必须让预处理器在它看到 ## 之前解析它。可以通过另一种方式间接强制解析它。让我们定义一个只包装 PASTEEVALUATING_PASTE 宏:

#define EVALUATING_PASTE(x,...) PASTE(x,__VA_ARGS__)

现在让我们用UNPAREN

#define UNPAREN(x) EVALUATING_PASTE(NOTHING_,EXTRACT x)

这是展开之后:

UNPAREN((int))
EVALUATING_PASTE(NOTHING_,EXTRACT (int))
PASTE(NOTHING_,EXTRACT int)
NOTHING_ ## EXTRACT int
NOTHING_EXTRACT int
int

即使没有额外加括号也能正常运行,因为额外的赋值并没有影响:

UNPAREN(int)
EVALUATING_PASTE(NOTHING_,EXTRACT int)
PASTE(NOTHING_,EXTRACT int)
NOTHING_ ## EXTRACT int
NOTHING_EXTRACT int
int

成功了!我们现在编写 GETTER 时可以不需要围绕类型的括号了:

#define GETTER(type,name) \
- (UNPAREN(type))name { \
return [_dictionary objectForKey: @#name]; \
}

奖励宏

在选择一些宏来证明这个结构时,我构建了一个很好的 dispatch_once 宏来制作延迟初始化的常量。实现如下:

#define ONCE(type,name,...) \
UNPAREN(type) name() { \
static UNPAREN(type) static_ ## name; \
static dispatch_once_t predicate; \
dispatch_once(&predicate,^{ \
static_ ## name = ({ __VA_ARGS__; }); \
}); \
return static_ ## name; \
}

使用案例:

ONCE(NSSet *,AllowedFileTypes,[NSSet setWithArray:@[ @"mp3",@"m4a",@"aiff" ]])

然后,你可以调用 AllowedFileTypes() 来获取集合,并根据需要高效创建集合。如果类型不巧包括括号,添加括号就能运行。

结论

仅仅写这个宏,我就发现了很多艰涩的知识。我希望接触这些知识也不会影响你的思维。请谨慎使用这些知识。

今天就这样。以后还会有更多令人兴奋的探索,可能比这还要再不可思议。在此之前,如果你对此主题有任何建议,请发送给 我们

本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 http://swift.gg

Swift Import 声明

作者:Mattt,原文链接,原文日期:2019-01-07
译者:雨谨;校对:numbbbbbYousanflics;定稿:Pancf

作为软件开发人员,我们学到的第一课是如何将概念和功能组织成独立的单元。在最小的层级上,这意味着思考类型、方法和属性。这些东西构成了模块(module)的基础,而模块又可以被打包成为 library 或者 framework。

在这种方式中,import 声明是将所有内容组合在一起的粘合剂。

尽管 import 声明非常重要,但大部分 Swift 开发者都只熟悉它的最基本用法:

import <#module#>

本周的 NSHipster 中,我们将探索 Swift 这个最重要的功能的其他用法。


import 声明允许你的代码访问其他文件中声明的符号。但是,如果多个模块都声明了一个同名的函数或类型,那么编译器将无法判断你的代码到底想调用哪个。

为了演示这个问题,考虑 铁人三项(Triathlon)铁人五项(Pentathlon) 这两个代表多运动比赛的模块:

铁人三项 包括三个项目:游泳、自行车和跑步。

// 铁人三项模块
func swim() {
print("🏊‍ Swim 1.5 km")
}

func bike() {
print("🚴 Cycle 40 km")
}

func run() {
print("🏃‍ Run 10 km")
}

铁人五项 模块由五个项目组成:击剑、游泳、马术、射击和跑步。

// 铁人五项模块
func fence() {
print("🤺 Bout with épées")
}

func swim() {
print("🏊‍ Swim 200 m")
}

func ride() {
print("🏇 Complete a show jumping course")
}

func shoot() {
print("🎯 Shoot 5 targets")
}

func run() {
print("🏃‍ Run 3 km cross-country")
}

如果我们单独 import 其中一个模块,我们可以通过它们的 非限定(unqualified)名称引用它们的每个函数,而不会出现问题。

import Triathlon

swim() // 正确,调用 Triathlon.swim
bike() // 正确,调用 Triathlon.bike
run() // 正确,调用 Triathlon.run

但是如果同时 import 两个模块,我们不能全部使用非限定函数名。铁人三项和五项都包括游泳和跑步,所以对 swim() 的引用是模糊的。

import Triathlon
import Pentathlon

bike() // 正确,调用 Triathlon.bike
fence() // 正确,调用 Pentathlon.fence
swim() // 错误,模糊不清

如何解决这个问题?一种策略是使用 全限定名称(fully-qualified name) 来处理任何不明确的引用。通过包含模块名称,程序是要在游泳池中游几圈,还是在开放水域中游一英里,就不存在混淆了。

import Triathlon
import Pentathlon

Triathlon.swim() // 正确,指向 Triathlon.swim 的全限定引用
Pentathlon.swim() // 正确,指向 Pentathlon.swim 的全限定引用

解决 API 名称冲突的另一种方法是更改 import 声明,使其更加严格地挑选需要包含每个模块哪些的内容。

import 单个声明

import 声明提供了一种样式,可以指定引入定义在顶层(top-level)的单个结构体、类、枚举、协议和类型别名,以及函数、常量和变量。

import <#kind#> <#module.symbol#>

这里,<#kind#> 可以为如下的任何关键字:

Kind Description
struct 结构体
class
enum 枚举
protocol 协议
typealias 类型别名
func 函数
let 常量
var 变量

例如,下面的 import 声明只添加了 Pentathlon 模块的 swim() 函数:

import func Pentathlon.swim

swim() // 正确,调用 Pentathlon.swim
fence() // 错误,无法解析的标识

解决符号名称冲突

当代码中多个符号被同一个名字被引用时,Swift 编译器参考以下信息,按优先级顺序解析该引用:

  1. 本地的声明
  2. 单个导入(import)的声明
  3. 整体导入的模块

如果任何一个优先级有多个候选项,Swift 将无法解决歧义,进而引发编译错误。

例如,整体导入的 Triathlon 模块会提供 swim()bike()run() 方法,但从 Pentathlon 中单个导入的 swim() 函数声明会覆盖 Triathlon 模块中的对应函数。同样,本地声明的 run() 函数会覆盖 Triathlon 中的同名符号,也会覆盖任何单个导入的函数声明。

import Triathlon
import func Pentathlon.swim

// 本地的函数会遮住整体导入的 Triathlon 模块
func run() {
print("🏃‍ Run 42.195 km")
}

swim() // 正确,调用 Pentathlon.swim
bike() // 正确,调用 Triathlon.bike
run() // 正确,调用本地的 run

那这个代码的运行结果是?一个古怪的多运动比赛,包括在一个泳池里游几圈的游泳,一个适度的自行车骑行,和一个马拉松跑。(@ 我们, 钢铁侠)

如果本地或者导入的声明,与模块的名字发生冲突,编译器首先查找声明,然后在模块中进行限定查找。

> import Triathlon
>
> enum Triathlon {
> case sprint, olympic, ironman
> }
>
> Triathlon.olympic // 引用本地的枚举 case
> Triathlon.swim() // 引用模块的函数
>

Swift编译器不会通知开发者,也无法协调模块和本地声明之间的命名冲突,因此使用依赖项时,你应该了解这种可能性。

澄清和缩小范围

除了解决命名冲突之外,import 声明还可以作为澄清程序员意图的一种方法。

例如,如果只使用 AppKit 这样大型框架中的一个函数,那么你可以在 import 声明中单独指定这个函数。

import func AppKit.NSUserName

NSUserName() // "jappleseed"

顶层常量和变量的来源通常比其他的导入符号更难识别,在导入它们时,这个技术尤其有用。

例如,Darwin framework 提供的众多功能中,包含一个顶层的 stderr 变量。这里的一个显式 import 声明可以在代码评审时,提前避免该变量来源的任何疑问。

import func Darwin.fputs
import var Darwin.stderr

struct StderrOutputStream: TextOutputStream {
mutating func write(_ string: String) {
fputs(string, stderr)
}
}

var standardError = StderrOutputStream()
print("Error!", to: &standardError)

import 子模块

最后一种 import 声明样式,提供了另一种限制 API 暴露的方式。

import <#module.submodule#>

你很可能在 AppKit 和 Accelerate 等大型的系统 framework 中遇到子模块。虽然这种 伞架构(umbrella framework) 不再是一种最佳实践,但它们在 20 世纪初苹果向 Cocoa 过渡的过程中发挥了重要作用。

例如,你可以仅 import Core Services frameworkDictionaryServices 子模块,从而将你的代码与无数已废弃的 API(如 Carbon Core)隔离开来。

import Foundation
import CoreServices.DictionaryServices

func define(_ word: String) -> String? {
let nsstring = word as NSString
let cfrange = CFRange(location: 0, length: nsstring.length)

guard let definition = DCSCopyTextDefinition(nil, nsstring, cfrange) else {
return nil
}

return String(definition.takeUnretainedValue())
}

define("apple") // "apple | ˈapəl | noun 1 the round fruit of a tree..."

事实上,单独导入的声明和子模块,除了澄清程序员的意图,并不能带来任何真正的好处。这种方式并不会让你的代码编译地更快。由于大部分的子模块似乎都会重新导入它们的伞头文件(umbrella header),因此这种方式也没法减少自动补全列表上的噪音。


与许多晦涩难懂的高级主题一样,你之所以没有听说过这些 import 声明样式,很可能的是因为你不需要了解它们。如果你已经在没有它们的情况下开发了很多 APP,那么你完全有理由可以相信,你不需要开始使用它们。

相反,这里比较有价值的收获是理解 Swift 编译器如何解决命名冲突。为此,理解 import 声明是非常重要的。

本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 http://swift.gg

用结构体和元组构建更整洁的类

作者:Benedikt Terhechte,原文链接,原文日期:2019-02-24
译者:WAMaker;校对:numbbbbbBigNerdCoding;定稿:Pancf

假设你正在开发一款社交网络应用,其中包含了一个带有关注按钮和点赞按钮的用户图片展示组件。同时,为了满足单一功能原则(single responsibility principle)和视图控制器的构成,点赞关注的实现应该另有它处。社交网络不仅有高级账户,也有企业账户,因此 InteractiveUserImageController(命名从来不是我的强项) 要能满足一系列的配置选项。以下是这个类一个可能的实现(为作展示,示例代码保留了不少可改进的地方):

final class InteractiveUserImageController: UIView {
/// 是否需要展示高级布局
var isPremium: Bool
/// 账户类型
var accountType: AccountType
/// 点击视图是否高亮
var isHighlighted: Bool
/// 用户名
var username: String
/// 用户头像
var profileImage: UIImage
/// 当前用户是否能点赞该用户
var canLike: Bool
/// 当前用户是否能关注该用户
var canFollow: Bool
/// 大赞按钮是否能使用
var bigLikeButton: Bool
/// 针对一些内容使用特殊的背景色
var alternativeBackgroundColor: Bool

init(...) {}
}

至此,我们就有了不少参数。随着应用体量的增长,会有更多的参数被加进类里。将这些参数通过职能进行划分和重构固然可行,但有时保持了单一功能后仍会有大量的参数存在。要如何才能更好的组织代码呢?

Swift 结构体结构

Swift 的 struct 类型在这种情况能发挥巨大的作用。依据参数的类型将它们装进一次性结构体:

final class InteractiveUserImageController: UIView {
struct DisplayOptions {
/// 大赞按钮是否能使用
var bigLikeButton: Bool
/// 针对一些内容使用特殊的背景色
var alternativeBackgroundColor: Bool
/// 是否需要展示高级布局
var isPremium: Bool
}
struct UserOptions {
/// 账户类型
var accountType: AccountType
/// 用户名
var username: String
/// 用户头像
var profileImage: UIImage
}
struct State {
/// 点击视图是否高亮
var isHighlighted: Bool
/// 当前用户是否能点赞该用户
var canLike: Bool
/// 当前用户是否能关注该用户
var canFollow: Bool
}

var displayOptions = DisplayOptions(...)
var userOptions = UserOptions(...)
var state = State(...)

init(...) {}
}

正如你所见,我们把这些状态放入了独立的 struct 类型中。不仅让类更整洁,也便于新上手的开发者找到相关联的选项。

已经是一个不错的改进了,但我们能做得更好!

我们面临的问题是查找一个参数需要额外的操作。

由于使用了一次性结构体类型,我们需要在某处定义它们(例如:struct DisplayOptions),也需要将它们实例化(例如:let displayOptions = DisplayOptions(...))。大体上来说没什么问题,但在更大的类中,为确定 displayOptions 的类型仍旧需要一次额外的查询。然而,与 C 语言不同,像下面这样的匿名 struct 在 Swift 里并不存在:

let displayOptions = struct {
/// 大赞按钮是否能使用
var bigLikeButton: Bool
/// 针对一些内容使用特殊的背景色
var alternativeBackgroundColor: Bool
/// 是否需要展示高级布局
var isPremium: Bool
}

元组 – 匿名结构体在 Swift 中的实现

实际上,Swift 中还真有这么一个类型。它就是我们的老朋友,tuple。自己看吧:

var displayOptions: (
bigLikeButton: Bool,
alternativeBackgroundColor: Bool,
isPremium: Bool
)

这里定义了一个新的类型 displayOptions,带有三个参数(bigLikeButtonalternativeBackgroundColorisPremium),它能像前面的 struct 一样被访问:

user.displayOptions.alternativeBackgroundColor = true

更好的是,参数定义不需要做额外的初始化,一切都井然有序。

强制不可变性

最后,tuple 既可以是 可变的 也可以是 不可变的。正如你在第一行所看到的那样:我们定义的是 var displayOptions 而不是 varlet bigLikeButtonbigLikeButtondisplayOptions 一样也是 var。这样做的好处在于强制把静态常量(例如行高,头部高度)放入一个不同的(let)组。

添加数据

当需要用一些值初始化参数时,你也能很好的利用这个特性,这是一个加分项:

var displayOptions = (
bigLikeButton: true,
alternativeBackgroundColor: false,
isPremium: false,
defaultUsername: "Anonymous"
)

与之前的代码类似,这里定义了一个元组的选项集,同时将它们正确进行了初始化。

简化

相比于使用结构体而言,使用了元组的选项集能更轻易的简化代码:

class UserFollowComponent {
var displayOptions = (
likeButton: (
bigButton: true,
alternativeBackgroundColor: true
),
imageView: (
highlightLineWidth: 2.0,
defaultColor: "#33854"
)
)
}

我希望这篇文章会对你有帮助。我大量应用这些简单的模式来让代码更具结构化。即便是只对 2 - 3 个参数做这样的处理,也能从中获益。

本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 http://swift.gg

千呼万唤始出来☑️:SwiftWebUI

作者:The Always Right Institute,原文链接,原文日期:2019-06-30
译者:Ji4n1ng;校对:numbbbbbWAMaker;定稿:Pancf


六月初,Apple 在 WWDC 2019 上发布了 SwiftUI。SwiftUI 是一个“跨平台的”、“声明式”框架,用于构建 tvOS、macOS、watchOS 和 iOS 上的用户界面。SwiftWebUI 则将它带到了 Web 平台上✔️。

免责声明:这是一个玩具项目!不要用于生产。使用 SwiftWebUI 是为了了解更多关于 SwiftUI 本身及其内部工作原理的信息。

SwiftWebUI

那么究竟什么是 SwiftWebUI?它允许你编写可以在 Web 浏览器中显示的 SwiftUI 的 视图

import SwiftWebUI

struct MainPage: View {
@State var counter = 0

func countUp() { counter += 1 }

var body: some View {
VStack {
Text("🥑🍞 #\(counter)")
.padding(.all)
.background(.green, cornerRadius: 12)
.foregroundColor(.white)
.tapAction(self.countUp)
}
}
}

结果是:

与其他一些工作不同,SwiftWebUI 不仅仅是将 SwiftUI 视图渲染为 HTML,而且还在浏览器和 Swift 服务器中托管的代码之间建立了一个连接,这样就可以实现各种交互功能——按钮、选择器、步进器、列表、导航等,这些都可以做到!

换句话说:SwiftWebUI 是针对浏览器的 SwiftUI API(很多部分但不是所有)的一种实现。

再次进行免责声明:这是一个玩具项目!不要用于生产。使用 SwiftWebUI 是为了了解更多关于 SwiftUI 本身及其内部工作原理的信息。

学习一次,随处使用

SwiftUI 的既定目标不是“编写一次,随处运行”,而是“学习一次,随处使用”。不要期望在 iOS 上开发了一个漂亮的 SwiftUI 应用程序,然后将它的代码放入 SwiftWebUI 项目中,并让它在浏览器中呈现完全相同的内容。这不是我们的重点。

关键是能够重用 SwiftUI 的原理并使其在不同平台之间共享。在这种情况下,SwiftWebUI 就达到目的了✔️。

但是先让我们深入了解一下细节,并编写一个简单的 SwiftWebUI 应用程序。本着“学习一次,随处使用”的精神,首先观看这两个 WWDC 演讲:介绍 SwiftUISwiftUI 要点。本文不会过多的深入数据流有关的内容,但这篇演讲同样推荐观看(这些概念在 SwiftWebUI 中被广泛支持):SwiftUI 中的数据流

要求

到目前为止,SwiftWebUI 需要安装 macOS Catalina 来运行(“Swift ABI”🤦‍♀️)。幸运的是,将 Catalina 安装在单独的 APFS 卷 上非常容易。并且需要安装 Xcode 11 才能获得在 SwiftUI 中大量使用的 Swift 5.1 新功能。明白了吗?很好!

Linux 呢?这个项目确实准备在 Linux 上运行,但尚未完成。唯一还没完成的事情是对 Combine PassthroughSubject 的简单实现以及围绕它的一些基础设施。准备:NoCombine。欢迎来提 PR!

Mojave 呢?有一个可以在 Mojave 和 Xcode 11 上运行的办法。你需要创建一个 iOS 13 模拟器项目并在其中运行整个项目。

开始第一个应用程序

创建 SwiftWebUI 项目

启动 Xcode 11,选择“File > New > Project…”或按 Cmd-Shift-N:

选择“macOS / Command Line Tool”项目模板:

给它取个好听的名字,用“AvocadoToast”吧:

然后,添加 SwiftWebUI 作为 Swift Package Manager 的依赖项。该选项隐藏在“File / Swift Packages”菜单中:

输入 https://github.com/SwiftWebUI/SwiftWebUI.git 作为包的 URL:

使用“Branch” master 选项,以便于总能获得最新和最好的版本(也可以使用修订版或 develop 分支):

最后,将 SwiftWebUI 库添加到你的工具的 target 中:

这就完成了创建。你现在有了一个可以导入 SwiftWebUI 的工具项目。(Xcode 可能需要一些时间来获取和构建依赖。)

SwiftWebUI Hello World

让我们开始使用 SwiftWebUI。打开 main.swift 文件,将其内容替换为:

import SwiftWebUI

SwiftWebUI.serve(Text("Holy Cow!"))

在 Xcode 中编译并运行该应用程序,打开 Safari,然后访问 http://localhost:1337/

这里发生了什么:首先导入 SwiftWebUI 模块(不要意外导入 macOS SwiftUI 😀)。

然后我们调用了 SwiftWebUI.serve,它要么接受一个返回视图的闭包,要么就直接是一个视图——如下所示:一个 Text 视图(也称为“UILabel”,它可以显示纯文本或格式化的文本)。

幕后发生的事情

在内部,serve 函数创建一个非常简单的 SwiftNIO HTTP 服务器,它将会监听 1337 端口。当浏览器访问该服务器时,它会创建一个 session(会话)并将(Text)视图传递给该会话。

最后,SwiftWebUI 在服务器上根据这个视图来创建一个“Shadow DOM”,将其渲染为 HTML 并将结果发送到浏览器。“Shadow DOM”(和状态对象保持在一起)存储在会话中。

这是 SwiftWebUI 应用程序与 watchOS 或 iOS SwiftUI 应用程序之间的区别。单个 SwiftWebUI 应用程序为一组用户提供服务,而不仅仅是一个用户。

添加一些交互

第一步,更好地组织代码。在项目中创建一个新的 Swift 文件,并将其命名为 MainPage.swift。然后向其中添加一个简单的 SwiftUI 视图的定义:

import SwiftWebUI

struct MainPage: View {

var body: some View {
Text("Holy Cow!")
}
}

修改 main.swift 来让 SwiftWebUI 作用于我们的定制视图:

SwiftWebUI.serve(MainPage())

现在,可以把 main.swift 放到一边,在自定义视图中完成所有工作。添加一些交互:

struct MainPage: View {
@State var counter = 3

func countUp() { counter += 1 }

var body: some View {
Text("Count is: \(counter)")
.tapAction(self.countUp)
}
}

视图 有了一个名为 counter 的持久 状态 变量(不知道这是什么?再看一下 SwiftUI 的介绍)。还有一个可以使计数器加一的小函数。

然后,使用 SwiftUI tapAction 修饰符将事件处理程序附加到 Text。最后,在标签中显示当前值:

🧙魔法🧙

幕后发生的事情

这是如何运作的?当浏览器访问端点时,SwiftWebUI 在其中创建了会话和“Shadow DOM”。然后将描述视图的 HTML 发送到浏览器。tapAction 通过向 HTML 添加 onclick 处理程序来工作。SwiftWebUI 还向浏览器发送 JavaScript(少量,没有大的 JavaScript 框架!),处理点击并将其转发到 Swift 服务器。

然后 SwiftUI 的魔法开始生效。SwiftWebUI 将 click 事件与“Shadow DOM”中的事件处理程序相关联,并调用 countUp 函数。该函数通过修改 counter 状态 变量,使视图的渲染无效。SwiftWebUI 开始工作,并对“Shadow DOM”中的变更进行差异比较。然后将这些变更发送回浏览器。

“变更”作为 JSON 数组发送,页面中的小型 JavaScript 可以处理这些数组。如果整个子树发生了变化(例如,如果用户导航到一个全新的视图),则变更可以是应用于 innerHTMLouterHTML 的更大的 HTML 片段。

但通常情况下,这些变更都很小,例如 添加类设置 HTML 属性 等(即浏览器 DOM 修改)。

🥑🍞 Avocado Toast

太好了,基础的部分可以正常工作了。让我们引入更多的交互。以下是基于 SwiftUI 要点 演讲中演示 SwiftUI 的“Avocado Toast App”。没看过吗?你应该看看,讲的是美味的吐司。

HTML / CSS 样式不漂亮也不完美。你知道,我们不是网页设计师,而且需要帮助。欢迎来提交 PR!

想要跳过细节,观看应用程序的 GIF 并在 GitHub 上下载:🥑🍞

🥑🍞订单

谈话从这(~6:00)开始,可以将这些代码添加到新的 OrderForm.swift 文件中:

struct Order {
var includeSalt = false
var includeRedPepperFlakes = false
var quantity = 0
}
struct OrderForm: View {
@State private var order = Order()

func submitOrder() {}

var body: some View {
VStack {
Text("Avocado Toast").font(.title)

Toggle(isOn: $order.includeSalt) {
Text("Include Salt")
}
Toggle(isOn: $order.includeRedPepperFlakes) {
Text("Include Red Pepper Flakes")
}
Stepper(value: $order.quantity, in: 1...10) {
Text("Quantity: \(order.quantity)")
}

Button(action: submitOrder) {
Text("Order")
}
}
}
}

main.swift 中直接用 SwiftWebUI.serve() 测试新的 OrderForm 视图。

这就是浏览器中的样子:

SemanticUI 用于在 SwiftWebUI 中设置一些样式。SemanticUI 并不是必须的,这里只是用它的控件来美化界面。

注意:仅使用 CSS 和字体,而不是 JavaScript 组件。

幕间休息:一些 SwiftUI 布局

SwiftUI 要点 演讲的 16:00 左右,他们将介绍 SwiftUI 布局和视图修改器排序:

var body: some View {
HStack {
Text("🥑🍞")
.background(.green, cornerRadius: 12)
.padding(.all)

Text(" => ")

Text("🥑🍞")
.padding(.all)
.background(.green, cornerRadius: 12)
}
}

结果如下,请注意修饰符的排序是如何相关的:

SwiftWebUI 尝试复制常见的 SwiftUI 布局,但还没有完全成功。毕竟它必须处理浏览器提供的布局系统。需要帮助,欢迎弹性盒布局相关的专家!

🥑🍞订单历史

回到应用程序,演讲(~19:50)介绍了 列表 视图,用于显示 Avocado toast 订单历史记录。这就是它在 Web 上的外观:

列表 视图遍历已完成订单的数组,并为每个订单创建一个子视图(OrderCell),并传入列表中的当前项。

这是我们使用的代码:

struct OrderHistory: View {
let previousOrders : [ CompletedOrder ]

var body: some View {
List(previousOrders) { order in
OrderCell(order: order)
}
}
}

struct OrderCell: View {
let order : CompletedOrder

var body: some View {
HStack {
VStack(alignment: .leading) {
Text(order.summary)
Text(order.purchaseDate)
.font(.subheadline)
.foregroundColor(.secondary)
}
Spacer()
if order.includeSalt {
SaltIcon()
}
else {}
if order.includeRedPepperFlakes {
RedPepperFlakesIcon()
}
else {}
}
}
}

struct SaltIcon: View {
let body = Text("🧂")
}
struct RedPepperFlakesIcon: View {
let body = Text("🌶")
}

// Model

struct CompletedOrder: Identifiable {
var id : Int
var summary : String
var purchaseDate : String
var includeSalt = false
var includeRedPepperFlakes = false
}

SwiftWebUI 列表视图效率很低,它总是呈现整个子集合。没有单元格重用,什么都没有😎。在一个网络应用程序中有各种各样的方法来处理这个问题,例如使用分页或更多客户端逻辑。

你不必手动输入演讲中的样本数据,我们为你提供了这些数据:

let previousOrders : [ CompletedOrder ] = [
.init(id: 1, summary: "Rye with Almond Butter", purchaseDate: "2019-05-30"),
.init(id: 2, summary: "Multi-Grain with Hummus", purchaseDate: "2019-06-02",
includeRedPepperFlakes: true),
.init(id: 3, summary: "Sourdough with Chutney", purchaseDate: "2019-06-08",
includeSalt: true, includeRedPepperFlakes: true),
.init(id: 4, summary: "Rye with Peanut Butter", purchaseDate: "2019-06-09"),
.init(id: 5, summary: "Wheat with Tapenade", purchaseDate: "2019-06-12"),
.init(id: 6, summary: "Sourdough with Vegemite", purchaseDate: "2019-06-14",
includeSalt: true),
.init(id: 7, summary: "Wheat with Féroce", purchaseDate: "2019-06-31"),
.init(id: 8, summary: "Rhy with Honey", purchaseDate: "2019-07-03"),
.init(id: 9, summary: "Multigrain Toast", purchaseDate: "2019-07-04",
includeSalt: true),
.init(id: 10, summary: "Sourdough with Chutney", purchaseDate: "2019-07-06")
]

🥑🍞涂抹酱选择器

选择器控件以及如何将它与枚举一起使用将在(~43:00)进行演示。首先是各种吐司选项的枚举:

enum AvocadoStyle {
case sliced, mashed
}

enum BreadType: CaseIterable, Hashable, Identifiable {
case wheat, white, rhy

var name: String { return "\(self)".capitalized }
}

enum Spread: CaseIterable, Hashable, Identifiable {
case none, almondButter, peanutButter, honey
case almou, tapenade, hummus, mayonnaise
case kyopolou, adjvar, pindjur
case vegemite, chutney, cannedCheese, feroce
case kartoffelkase, tartarSauce

var name: String {
return "\(self)".map { $0.isUppercase ? " \($0)" : "\($0)" }
.joined().capitalized
}
}

可以将这些代码添加到 Order 结构体中:

struct Order {
var includeSalt = false
var includeRedPepperFlakes = false
var quantity = 0
var avocadoStyle = AvocadoStyle.sliced
var spread = Spread.none
var breadType = BreadType.wheat
}

然后使用不同的选择器类型来显示它们。如何循环枚举值非常简单:

Form {
Section(header: Text("Avocado Toast").font(.title)) {
Picker(selection: $order.breadType, label: Text("Bread")) {
ForEach(BreadType.allCases) { breadType in
Text(breadType.name).tag(breadType)
}
}
.pickerStyle(.radioGroup)

Picker(selection: $order.avocadoStyle, label: Text("Avocado")) {
Text("Sliced").tag(AvocadoStyle.sliced)
Text("Mashed").tag(AvocadoStyle.mashed)
}
.pickerStyle(.radioGroup)

Picker(selection: $order.spread, label: Text("Spread")) {
ForEach(Spread.allCases) { spread in
Text(spread.name).tag(spread) // there is no .name?!
}
}
}
}

结果是:

同样,这需要一些对 CSS 的热爱来让它看起来更好看…

完成后的🥑🍞应用

不,我们与原版略有不同,也没有真正完成应用。它看起来并不那么棒,但毕竟只是一个演示示例😎。

完成后的应用程序可在GitHub:AvocadoToast 上获取。

HTML 和 SemanticUI

UIViewRepresentable 在 SwiftWebUI 中对应的实现,是直接使用原始 HTML。

它提供了两种变体,一种是 HTML 按原样输出字符串,另一种是通过 HTML 转义内容:

struct MyHTMLView: View {
var body: some View {
VStack {
HTML("<blink>Blinken Lights</blink>")
HTML("42 > 1337", escape: true)
}
}
}

使用这个原语,基本上可以构建所需的任何 HTML。

还有一种更高级的用法是 HTMLContainer,SwiftWebUI 内部也用到了它。例如,这是步进器控件的实现:

var body: some View {
HStack {
HTMLContainer(classes: [ "ui", "icon", "buttons", "small" ]) {
Button(self.decrement) {
HTMLContainer("i", classes: [ "minus", "icon" ], body: {EmptyView()})
}
Button(self.increment) {
HTMLContainer("i", classes: [ "plus", "icon" ], body: {EmptyView()})
}
}
label
}
}

HTMLContainer 是“响应式的”,即如果类、样式或属性发生变化,它将触发(emit)常规 DOM 变更(而不是重新渲染整个内容)。

SemanticUI

SwiftWebUI 还附带了一些预先设置的 SemanticUI 控件:

VStack {
SUILabel(Image(systemName: "mail")) { Text("42") }
HStack {
SUILabel(Image(...)) { Text("Joe") } ...
}
HStack {
SUILabel(Image(...)) { Text("Joe") } ...
}
HStack {
SUILabel(Image(...), Color("blue"),
detail: Text("Friend"))
{
Text("Veronika")
} ...
}
}

……渲染为如下内容:

请注意,SwiftWebUI 还支持一些 SFSymbols 图像名称(通过 Image(systemName:) 来使用)。这些都得到了 SemanticUI 对 Font Awesome 的支持

还有 SUISegmentSUIFlagSUICARD

SUICards {
SUICard(Image.unsplash(size: UXSize(width: 200, height: 200),
"Zebra", "Animal"),
Text("Some Zebra"),
meta: Text("Roaming the world since 1976"))
{
Text("A striped animal.")
}
SUICard(Image.unsplash(size: UXSize(width: 200, height: 200),
"Cow", "Animal"),
Text("Some Cow"),
meta: Text("Milk it"))
{
Text("Holy cow!.")
}
}

……渲染为这些内容:

添加此类视图非常简单,也非常有趣。可以使用 WOComponent 的 SwiftUI 视图来快速构建相当复杂和美观的布局。

Image.unsplash 根据 http://source.unsplash.com 上运行的 Unsplash API 来构建图像的查询。只需给它一些查询词、大小和可选范围。

注意:有时,特定的 Unsplash 服务似乎有点慢且不可靠。

总结

这就是我们的演示示例。我们希望你能喜欢!但要再次进行免责声明:这是一个玩具项目!不要用于生产。使用 SwiftWebUI 是为了了解更多关于 SwiftUI 本身及其内部工作原理的信息。

我们认为它是一个很好的玩具,可能也是一个有价值的工具,以便于更多地了解 SwiftUI 的内部工作原理。

技术随记

这些只是关于该技术的各个方面的一些笔记。可以跳过,这个不是那么的有趣😎。

问题

SwiftWebUI 有很多问题,有些是在 GitHub 上提出的:Issues。欢迎来提更多问题。

相当多的 HTML 布局的东西有问题(例如 ScrollView 并不总是滚动的),还有一些像 Shapes 这样的正在讨论方案的功能也有问题(可能通过 SVG 和 CSS 很容易做到)。

哦,还有一个例子是 If-ViewBuilder 不能正常工作。不明白为什么:

var body: some View {
VStack {
if a > b {
SomeView()
}
// currently need an empty else: `else {}` to make it compile.
}
}

需要帮忙!欢迎来提交 PR!

与原来的 SwiftUI 相比

本文的实现非常简单且效率低下。在现实情况下,必须以更高的速率来处理状态修改事件,以 60Hz 的帧速率做所有的动画等等。

我们侧重于使基本操作正确,例如状态和绑定如何工作,视图如何以及何时更新等等。很可能本文的实现在某些方面并不正确,可能是因为 Apple 忘了将原始资源作为 Xcode 11 的一部分发送给我们。

WebSockets

我们目前使用 AJAX 将浏览器连接到服务器。使用 WebSockets 有多种优势:

  • 保证了事件的顺序(AJAX 请求可能不同步到达)
  • 非用户发起的服务器端 DOM 更新(定时器、推送)
  • 会话超时指示器

这会让实现一个聊天客户端的演示示例变得非常容易。

添加 WebSockets 实际上非常简单,因为事件已经作为 JSON 发送了。我们只需要客户端和服务器端的垫片(shims)。所有这些都已经在 swift-nio-irc-webclient 中试用过了,只需要移植一下。

SPA

SwiftWebUI 的当前版本是一个连接到有状态后端服务器的 SPA(单页面应用程序)。

还有其他方法可以做到这一点,例如,当用户通过正常的链接遍历应用程序时,保持树的状态。又名 WebObjects。;-)

一般来说,最好能更好地控制 DOM ID 生成、链接生成以及路由等等。这和 SwiftObjects 所提供的方式类似。

但是最终用户将不得不放弃很多本可以“学习一次,随处使用”的功能,因为 SwiftUI 操作处理程序通常是围绕着捕捉任意状态的事实来构建的。

我们将会期待基于 Swift 的服务器端框架提出什么更好的东西来👽。

WASM

一旦我们找到合适的 Swift WASM(WebAssembly),SwiftWebUI 就会更有用处。期待 WASM!

WebIDs

有些像 ForEach 这样的 SwiftUI 视图需要 Identifiable 对象,其中的 id 可以是任何 Hashable。这在 DOM 中不太好,因为我们需要基于字符串的 ID 来识别节点。

这是通过将 ID 映射到全局映射中的字符串来解决的。这在技术上是无界的(一个类引用的特定问题)。

总结:对于 web 代码,最好使用字符串或整型来标识个体。

表单

表单需要做得更好:Issue

SemanticUI 有一些很好的表单布局,我们可能参照这些布局重写子树。有待商榷。

面向 Swift 的 WebObjects 6

花了点时间在文章中嵌入了下面这个可点击的 Twitter 控件。(译者注:由于某些原因,这里没办法像原文一样嵌入 Twitter 控件,只能放链接。)

https://twitter.com/helje5/status/1137092138104233987/photo/1?ref_src=twsrc%5Etfw%7Ctwcamp%5Etweetembed%7Ctwterm%5E1137092138104233987&ref_url=http%3A%2F%2Fwww.alwaysrightinstitute.com%2Fswiftwebui%2F

苹果确实给了我们一个“Swift 风格”的 WebObjects 6!

下一篇:直面 Web 和一些 Swift 化的 EOF(又名 CoreData 又名 ZeeQL)。

链接

联系方式

嘿,我们希望你能喜欢这篇文章,并且也希望得到你的反馈!

Twitter(任何一个都可以):@helje5@ar_institute

电子邮件:wrong@alwaysrightinstitute.com

Slack:在 SwiftDE、swift-server、noze、ios-developers 上找到我们。

写于 2019 年 6 月 30 日

本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 http://swift.gg

Swift 中的面向协议编程:引言

作者:Andrew Jaffee,原文链接,原文日期:2018-03-20
译者:灰s;校对:numbbbbbWAMaker;定稿:Pancf

对于开发者来说,复杂性是最大的敌人,因此我会去了解那些可以帮助我管理混乱的新技术。Swift 中的“面向协议编程”(POP)是最近(至少自2015年以来)引起广泛关注的“热门”方法之一。在这里我们将使用 Swift 4。在我自己编写代码时,发现 POP 很有前途。更吸引人的是,Apple 宣称 “Swift 的核心是面对协议的”。我想在一个正式的报告中分享关于 POP 的经验,一篇关于这个新兴技术清晰而简洁的教程。

我将解释关键概念,提供大量代码示例,无法避免的将 POP 和 OOP (面向对象编程)进行比较,并对面向流行编程(FOP?)的人群所声称的 POP 是解决所有问题的灵丹妙药这一说法进行泼冷水。

面向协议编程是一个很棒的新工具,值得添加到你现有的编程工具库中,但是没有什么可以代替那些经久不衰的基本功,就像将大的函数拆分成若干个小函数,将大的代码文件拆分成若干个小的文件,使用有意义的变量名,在敲代码之前花时间设计架构,合理而一致的使用间距和缩进,将相关的属性和行为分配到类和结构体中 - 遵循这些常识可以让世界变得不同。如果你编写的代码无法被同事理解,那它就是无用的代码。

学习和采用像 POP 这样的新技术并不需要绝对的唯一。POP 和 OOP 不仅可以共存,还可以互相协助。对于大多数开发者包括我自己,掌握 POP 需要时间和耐心。因为 POP 真的很重要,所以我将教程分成两篇文章。本文将主要介绍和解释 Swift 的协议和 POP。第二篇文章将深入研究 POP 的高级应用方式(比如从协议开始构建应用程序的功能),范型协议,从引用类型到值类型转变背后的动机,列举 POP 的利弊,列举 OOP 的利弊,比较 OOP 和 POP,阐述为什么“Swift 是面向协议的”,并且深入研究一个被称为 “局部推理” 的概念,它被认为是通过使用 POP 增强的。这次我们只会粗略涉及一些高级主题。

引言

作为软件开发者,管理复杂性本质上是我们最应该关注的问题。当我们尝试学习 POP 这项新技术时,你可能无法从时间的投资中看到即时回报。但是,就像你对我的认识有个过程一样,你将会了解 POP 处理复杂性的方法,同时为你提供另一种工具来控制软件系统中固有的混乱。

我听到越来越多关于 POP 的讨论,但是却很少看到使用这种方式编写的产品代码,换句话说,我还没有看到有很多人从协议而不是类开始创建应用程序的功能。这不仅仅是因为人类有抗拒改变的倾向。学习一种全新的范式并将其付诸实践,说起来容易做起来难。在我编写新应用程序时,逐渐发现自己开始使用 POP 来设计和实现功能 — 有组织的且自然而然的。

伴随着新潮流带来的刺激,很多人都在谈论用 POP 取代 OOP。我认为除非像 Swift 这样的 POP 语言被广泛改进,否则这是不可能发生的 — 也或许根本就不会发生。我是个实用主义者,而不是追求时髦的人。在开发新的 Swift 项目时,我发现自己的行为是一种折衷的方法。我在合理的地方利用 OOP,而用 POP 更合适的地方也不会死脑筋的一定要使用 OOP,这样反而了解到这两种模式并不相互排斥。我把这两种技术结合在一起。在本期由两部分组成的 POP 教程中,你将了解我在说什么。

我投入到 OOP 中已经有很久了。1990 年,我买了一个零售版本的 Turbo Pascal。在使用了 OOP 大约一年后,我开始设计、开发和发布面向对象的应用程序产品。我成了一个忠粉。当我发现可以扩展增强自己的类,简直兴奋的飞起。随着时间的推移,Microsoft 和 Apple 等公司开始开发基于 OOP 的大型代码库,如 Microsoft Foundation Classes(MFC)和 .NET,以及 iOS 和 OS X SDK。现在,开发人员在开发新应用程序时很少需要重新造轮子。没有完美的方法,OOP 也有一些缺点,但是优点仍然大于缺点。我们将花一些时间来比较 OOP 和 POP。

理解协议

当开发人员设计一个新的 iOS 应用程序的基本结构时,他们几乎总是从 FoundationUIKit 等框架中的现有 开始。我能想到的几乎所有应用程序都需要某种用户界面导航系统。用户需要一些进入应用程序的入口点和引导他们使用应用程序功能的路标。可以浏览一下你的 iPhone 或 iPad 上的应用程序。

当这些应用程序打开时,你看到了什么?我打赌你看到的是 UITableViewControllerUICollectionViewControllerUIPageViewController 的子类。

当你第一次创建新的 iOS 项目时,所有人都必须认识下面的代码片段,例如,一个新的 iOS 项目基于 Xcode 中的 Single View App(单视图应用) 模板:

...
import UIKit

class ViewController: UIViewController
{
...

部分开发人员将在这里停下来,创建完全定制的接口,但大多数人将采取另一个步骤。

当 iOS 开发者开发新的应用程序时,最常见的特征就是 OOP,那么 POP 在这里扮演什么角色呢?

你知道我将怎样继续么?想象大多数开发人员的下一个主要步骤是什么。那就是遵循协议(并实现 委托,但我们已经讨论过了)。

让我给你们看一个例子使其便于理解。我相信你们很多人都用过 UITableView。虽然这不是一个关于 UITableView 的教程,但是你应该知道在 UIViewController 中将其实现时,协议扮演着重要的角色。在向 UIViewController 中添加 UITableView时,UIViewController 必须遵循 UITableViewDataSourceUITableViewDelegate 协议,就像这样:

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate

简而言之,遵循 UITableViewDataSource 允许你用数据填充所有的 UITableViewCell,比如给用户提供导航的菜单项名称。采用 UITableViewDelegate,你可以对用户与 UITableView 的交互进行更细粒度的控制,比如在用户点击特定的 UITableViewCell 时执行适当的操作。

定义

我发现,在进行技术性定义和讨论之前,理解常用的术语定义可以帮助读者更好地理解某个主题。首先,让我们 考虑 “协议”一词的通俗定义

……管理国家事务或外交领域的正式程序或规则体系。……
在任何团体、组织或形势下,公认或已制定的程序或行为准则。……
进行科学实验时的程序……

Apple 的“Swift 编程语言(Swift 4.0.3)” 文档中的声明

协议定义了适合特定任务或功能的方法、属性和其他需求的蓝图。然后,类、结构体或枚举可以遵循该协议来提供这些需求的实际实现。任何满足协议要求的类型都被称为遵循该协议。

协议是最重要的工具之一,我们必须给软件固有的混乱带来一些秩序。协议使我们能够要求一个或多个类和结构体包含特定的最小且必需的属性,和/或提供特定的最小且必需的实现/方法。通过 协议扩展,我们可以为一些或所有协议的方法提供默认实现。

遵循协议

下面,我们将使自定义的 Person遵循采用)Apple 自带 Equatable 协议。

遵循 Equatable 协议以后可以使用等于运算符(==)来判断是否相等,使用不等于运算符(!=)来判断是否不等。Swift 标准库中的大部分基础类型都遵循了 Equatable 协议……

class Person : Equatable
{
var name:String
var weight:Int
var sex:String

init(weight:Int, name:String, sex:String)
{
self.name = name
self.weight = weight
self.sex = sex
}

static func == (lhs: Person, rhs: Person) -> Bool
{
if lhs.weight == rhs.weight &&
lhs.name == rhs.name &&
lhs.sex == rhs.sex
{
return true
}
else
{
return false
}
}
}

Apple 规定,“自定义类型声明它们采用特定的协议,需要将协议的名称放在类型名称之后,以冒号分隔,作为其定义的一部分。”这也正是我所做的:

class Person : Equatable

你可以将协议理解为专门针对 classstructenum约定承诺。我通过 Equatable 协议使自定义的 Person 类遵守了一个约定,Person承诺通过现实 Equatable 协议需要的方法或成员变量来履行该约定,即将其实现。

Equatable 协议并没有实现任何东西。它只是指明了采用(遵循) Equatable 协议的 classstruct,或者 enum 必须实现的方法和/或成员变量。有一些协议通过 extensions 实现了功能,稍后我们会进行讨论。我不会花太多时间来讲述关于 enum 的 POP 用法。我将它作为练习留给你。

定义协议

理解协议最好的方式是通过例子。我将自己构建一个 Equatable 来向你展示协议的用法:

protocol IsEqual
{
static func == (lhs: Self, rhs: Self) -> Bool

static func != (lhs: Self, rhs: Self) -> Bool
}

请记住,我的“IsEqual”协议并没有对 ==!= 运算符进行实现。“IsEqual”需要协议的遵循者实现他们自己的 ==!= 运算符。

所有定义协议属性和方法的规则都在 Apple 的 Swift 文档 中进行了总结。比如,在协议中定义属性永远不要用 let 关键字。只读属性规定使用 var 关键字,并在后面单独跟上 { get }。如果有一个方法改变了一个或多个属性,你需要标记它为 mutating。你需要知道为什么我重写的 ==!= 操作符被定义为 static。如果你不知道,找出原因将会是一个很好的练习。

为了向你展示我的 IsEqual(或者 Equatable)这样的协议具有广泛的适用性,我们将使用它在下面构建一个类。但是在我们开始之前,让我们先讨论一下“引用类型”与“值类型”。

引用类型与值类型

在继续之前,您应该阅读 Apple 关于 “值和引用类型” 的文章。它将让你思考引用类型和值类型。我故意不在这里讲太多细节,因为我想让你们思考并理解这个非常重要的概念。它太过重要,以至于针对 POP 引用/值类型的讨论同时出现在这些地方:

  1. WWDC 2015 展示的 “Protocol-Oriented Programming in Swift”
  2. WWDC 2015 展示的 “Building Better Apps with Value Types in Swift”
  3. WWDC 2016 展示的 “Protocol and Value Oriented Programming in UIKit Apps”

我会给你一个提示和作业……假设你有多个指向同一个类实例的引用,用于修改或“改变”属性。这些引用指向相同的数据块,因此将其称为“共享”数据并不夸张。在某些情况下,共享数据可能会导致问题,如下面的示例所示。这是否表示我们要将所有的代码改成值类型?并不是!就像 Apple 的一个工程师指出:“例如,以 Window 为例。复制一个 Window 是什么意思?” 查看下面的代码,并思考这个问题。

引用类型

下面的代码片段来自 Xcode playground,在创建对象副本然后更改属性时,会遇到一个有趣的难题。你能找到问题么?我们将在下一篇文章中讨论这个问题。

这段代码同时也演示了协议的定义和 extension

// 引用类型:每个人都使用类很长时间了 
// -- 想想 COCOA 中进行的所有隐式复制。

protocol ObjectThatFlies
{
var flightTerminology: String { get }
func fly() // 不需要提供实现,除非我想
}

extension ObjectThatFlies
{
func fly() -> Void
{
let myType = String(describing: type(of: self))
let flightTerminologyForType = myType + " " + flightTerminology + "\n"
print(flightTerminologyForType)
}
}

class Bird : ObjectThatFlies
{
var flightTerminology: String = "flies WITH feathers, and flaps wings differently than bats"
}

class Bat : ObjectThatFlies
{
var flightTerminology: String = "flies WITHOUT feathers, and flaps wings differently than birds"
}

// 引用类型

let bat = Bat()
bat.fly()
// "Bat flies WITHOUT feathers, and flaps wings differently than birds"

let bird = Bird()
bird.fly()
// "Bird flies WITH feathers, and flaps wings differently than bats"

var batCopy = bat
batCopy.fly()
// "Bird flies WITH feathers, and flaps wings differently than bats"

batCopy.flightTerminology = ""
batCopy.fly()
// 控制台输出 "Bat"

bat.fly()
// 控制台输出 "Bat"

来自前面代码片段的控制台输出

Bat flies WITHOUT feathers, and flaps wings differently than birds

Bird flies WITH feathers, and flaps wings differently than bats

Bird flies WITH feathers, and flaps wings differently than bats

Bat

Bat

值类型

在接下来的 Swift 代码片段中,我们使用 struct 替代 class。在这里,代码看起来更安全,而 Apple 似乎在推广值类型和 POP。注意,他们目前还没有放弃 class

// 这是范式转变的起点,不仅仅是协议,还有值类型

protocol ObjectThatFlies
{
var flightTerminology: String { get }
func fly() // 不需要提供实现,除非我想
}

extension ObjectThatFlies
{
func fly() -> Void
{
let myType = String(describing: type(of: self))
let flightTerminologyForType = myType + " " + flightTerminology + "\n"
print(flightTerminologyForType)
}
}

struct Bird : ObjectThatFlies
{
var flightTerminology: String = "flies WITH feathers, and flaps wings differently than bats"
}

struct Bat : ObjectThatFlies
{
var flightTerminology: String = "flies WITHOUT feathers, and flaps wings differently than birds"
}

// 值类型

let bat = Bat()
bat.fly()
// "Bat flies WITHOUT feathers, and flaps wings differently than birds"

let bird = Bird()
bird.fly()
// "Bird flies WITH feathers, and flaps wings differently than bats"

var batCopy = bat
batCopy.fly()
// "Bird flies WITH feathers, and flaps wings differently than bats"

// 我在这里对 Bat 实例所做的事情是显而易见的
batCopy.flightTerminology = ""
batCopy.fly()
// 控制台输出 "Bat"

// 但是,因为我们使用的是值类型,所以 Bat 实例的原始数据并没有因为之前的操作而被篡改。
bat.fly()
// "Bat flies WITHOUT feathers, and flaps wings differently than birds"

来自前面代码片段的控制台输出

Bat flies WITHOUT feathers, and flaps wings differently than birds

Bird flies WITH feathers, and flaps wings differently than bats

Bat flies WITHOUT feathers, and flaps wings differently than birds

Bat

Bat flies WITHOUT feathers, and flaps wings differently than birds

示例代码

我写了一些面向协议的代码。请通读代码,阅读内联注释,阅读附带的文章,跟随我的超链接,并充分理解我在做什么。你将在下一篇关于 POP 的文章中用到它。

采用多种协议

刚开始写这篇文章的时候,我很贪心,想要自定义一个协议,使它能同时体现 Apple 的内置协议 EquatableComparable

protocol IsEqualAndComparable
{

static func == (lhs: Self, rhs: Self) -> Bool

static func != (lhs: Self, rhs: Self) -> Bool

static func > (lhs: Self, rhs: Self) -> Bool

static func < (lhs: Self, rhs: Self) -> Bool

static func >= (lhs: Self, rhs: Self) -> Bool

static func <= (lhs: Self, rhs: Self) -> Bool

}

我意识到应该将它们分开,使我的代码尽可能灵活。为什么不呢?Apple 声明同一个类,结构体,枚举可以遵循多个协议,就像接下来我们将看到的一样。下面是我提出的两个协议:

protocol IsEqual
{
static func == (lhs: Self, rhs: Self) -> Bool

static func != (lhs: Self, rhs: Self) -> Bool
}

protocol Comparable
{
static func > (lhs: Self, rhs: Self) -> Bool

static func < (lhs: Self, rhs: Self) -> Bool

static func >= (lhs: Self, rhs: Self) -> Bool

static func <= (lhs: Self, rhs: Self) -> Bool
}

记住你的算法

你需要磨练的一项重要技能是编程的算法,并将它们转换为代码。我保证在将来的某一天,会有人给你一个复杂过程的口头描述并要求你对它进行编码。用人类语言描述某些步骤,之后用软件将其实现,它们之间一般都会有很大的差距。当我想要将 IsEqualComparable 应用于表示直线(向量)的类时,我意识到了这一点。我记得计算一个直线的长度是基于勾股定理的(参考 这里这里),并且对向量使用 ==!=<><=,和 >= 这些运算符进行比较时,直线的长度是必须的。我的 Line 类迟早会派上用场,例如,在一个绘图应用程序或游戏中,用户点击屏幕上的两个位置,在两点之间创建一条线。

自定义类采用多个协议

这是我的 Line 类,它采用了两个协议,IsEqualComparable(如下)。这是多继承的一种形式!

class Line : IsEqual, Comparable
{
var beginPoint:CGPoint
var endPoint:CGPoint

init()
{
beginPoint = CGPoint(x: 0, y: 0);
endPoint = CGPoint(x: 0, y: 0);
}

init(beginPoint:CGPoint, endPoint:CGPoint)
{
self.beginPoint = CGPoint( x: beginPoint.x, y: beginPoint.y )
self.endPoint = CGPoint( x: endPoint.x, y: endPoint.y )
}

// 线长的计算基于勾股定理。
func length () -> CGFloat
{
let length = sqrt( pow(endPoint.x - beginPoint.x, 2) + pow(endPoint.y - beginPoint.y, 2) )
return length
}

static func == (leftHandSideLine: Line, rightHandSideLine: Line) -> Bool
{
return (leftHandSideLine.length() == rightHandSideLine.length())
}

static func != (leftHandSideLine: Line, rightHandSideLine: Line) -> Bool
{
return (leftHandSideLine.length() != rightHandSideLine.length())
}

static func > (leftHandSideLine: Line, rightHandSideLine: Line) -> Bool
{
return (leftHandSideLine.length() > rightHandSideLine.length())
}

static func < (leftHandSideLine: Line, rightHandSideLine: Line) -> Bool
{
return (leftHandSideLine.length() < rightHandSideLine.length())
}

static func >= (leftHandSideLine: Line, rightHandSideLine: Line) -> Bool
{
return (leftHandSideLine.length() >= rightHandSideLine.length())
}

static func <= (leftHandSideLine: Line, rightHandSideLine: Line) -> Bool
{
return (leftHandSideLine.length() <= rightHandSideLine.length())
}

} // 类的结束行:IsEqual, Comparable

验证你的算法

我使用电子制表软件 Apple Numbers,并准备了两个向量的可视化表示,对 Line 类的 length() 方法做了一些基本测试:

这里是我根据上面图表中的点,写的测试代码:

let x1 = CGPoint(x: 0, y: 0)
let y1 = CGPoint(x: 2, y: 2)
let line1 = Line(beginPoint: x1, endPoint: y1)
line1.length()
// returns 2.82842712474619

let x2 = CGPoint(x: 3, y: 2)
let y2 = CGPoint(x: 5, y: 4)
let line2 = Line(beginPoint: x2, endPoint: y2)
line2.length()
// returns 2.82842712474619

line1 == line2
// returns true
line1 != line2
// returns false
line1 > line2
// returns false
line1 <= line2
// returns true

使用 Xcode “Single View” playground 模版测试/原型化 UI

你是否知道可以使用 Xcode 9 Single View playground 模板来原型化和测试用户界面(UI)?它非常棒 - 可以节省大量时间并快速原型化的工具。为了更好的测试我的 Line 类,我创建了这样一个 playground。作业:在我解释之前,我想让你自己试一下。向你展示我的 playground 代码、模拟器输出和我的 Swift 测试语句。

这里是我的 playground 代码:

import UIKit
import PlaygroundSupport

class LineDrawingView: UIView
{
override func draw(_ rect: CGRect)
{
let currGraphicsContext = UIGraphicsGetCurrentContext()
currGraphicsContext?.setLineWidth(2.0)
currGraphicsContext?.setStrokeColor(UIColor.blue.cgColor)
currGraphicsContext?.move(to: CGPoint(x: 40, y: 400))
currGraphicsContext?.addLine(to: CGPoint(x: 320, y: 40))
currGraphicsContext?.strokePath()

currGraphicsContext?.setLineWidth(4.0)
currGraphicsContext?.setStrokeColor(UIColor.red.cgColor)
currGraphicsContext?.move(to: CGPoint(x: 40, y: 400))
currGraphicsContext?.addLine(to: CGPoint(x: 320, y: 60))
currGraphicsContext?.strokePath()

currGraphicsContext?.setLineWidth(6.0)
currGraphicsContext?.setStrokeColor(UIColor.green.cgColor)
currGraphicsContext?.move(to: CGPoint(x: 40, y: 400))
currGraphicsContext?.addLine(to: CGPoint(x: 250, y: 80))
currGraphicsContext?.strokePath()
}
}

class MyViewController : UIViewController
{
override func loadView()
{
let view = LineDrawingView()
view.backgroundColor = .white

self.view = view
}
}

// 在实时视图窗口中显示视图控制器
PlaygroundPage.current.liveView = MyViewController()

这是我在 playground 模拟器上的视图输出:

下面是测试我的 Line 类型实例与我在 playground 上所画向量匹配的 Swift 代码:

let xxBlue = CGPoint(x: 40, y: 400)
let yyBlue = CGPoint(x: 320, y: 40)
let lineBlue = Line(beginPoint: xxBlue, endPoint: yyBlue)

let xxRed = CGPoint(x: 40, y: 400)
let yyRed = CGPoint(x: 320, y: 60)
let lineRed = Line(beginPoint: xxRed, endPoint: yyRed)
lineRed.length()
// returns 440.454310910905

lineBlue != lineRed
// returns true
lineBlue > lineRed
// returns true
lineBlue >= lineRed
// returns true

let xxGreen = CGPoint(x: 40, y: 400)
let yyGreen = CGPoint(x: 250, y: 80)
let lineGreen = Line(beginPoint: xxGreen, endPoint: yyGreen)
lineGreen.length()
// returns 382.753184180093
lineGreen < lineBlue
// returns true
lineGreen <= lineRed
// returns true
lineGreen > lineBlue
// returns false
lineGreen >= lineBlue
// returns false
lineGreen == lineGreen
// returns true

总结

我希望你喜欢今天的文章,并且非常期待阅读本文的“第二部分”。记住,我们将深入研究使用 POP 的先进应用程序,范型协议,从引用类型到值类型背后的动机,列举 POP 的优缺点,列举 OOP 的优缺点,比较 OOP 和 POP,确定为什么“Swift 是面向协议的”,并深入研究称为“局部推理”的概念。

本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 http://swift.gg

Swift 关键字

作者:Jordan Morgan,原文链接,原文日期:2017-02-11
译者:郑一一;校对:numbbbbbpmst;定稿:Pancf

有句话之前我提过,今天还想再说一次。那就是打铁还需自身硬。对于自身能力的严格要求,可以帮助实现我们所有梦寐以求的东西。

说起来可能有些消极,知识毕竟是永远学不完的。不论如何,今天 我们先来学习一下 Swift 中的每一个关键字(V3.0.1),在介绍每个关键字的时候,同时会附带一段代码加以说明。

在这些关键字之中,会有你熟悉或者不熟悉的部分。但为了最好的阅读和学习体验,我把它们全部列出来了。文章篇幅有些长,你准备好了么?

让我们现在就开始吧。

声明式关键字

associatedtype:在协议中,定义一个类型的占位符名称。直到协议被实现,该占位符才会被指定具体的类型。

protocol Entertainment  
{
associatedtype MediaType
}

class Foo : Entertainment
{
typealias MediaType = String //可以指定任意类型
}

class:通用、灵活的结构体,是程序的基础组成部分。与 struct 类似,不同之处在于:

  • 允许一个类继承另一个类的特性。
  • 类型转换,允许在运行时检查和指定一个类的实际类型。
  • 析构方法允许类的实例释放所有资源。
  • 引用计数允许多个引用指向一个实例。
class Person  
{
var name:String
var age:Int
var gender:String
}

deinit:当一个类的实例即将被销毁时,会调用这个方法。

class Person  
{
var name:String
var age:Int
var gender:String

deinit
{
//从堆中释放,并释放的资源
}
}

enum:定义了包含一组有关联的值的类型,并可以以一种类型安全的方式使用这些值。在 Swift 中,枚举是一等类型,拥有在其它语言中只有 class 才会支持的特性。

enum Gender  
{
case male
case female
}

extension:允许给已有的类、结构体、枚举、协议类型,添加新功能。

class Person  
{
var name:String = ""
var age:Int = 0
var gender:String = ""
}

extension Person
{
func printInfo()
{
print("My name is \(name), I'm \(age) years old and I'm a \(gender).")
}
}

fileprivate:访问控制权限,只允许在定义源文件中访问。

class Person  
{
fileprivate var jobTitle:String = ""
}

extension Person
{
//当 extension 和 class 在同一个文件中时,允许访问
func printJobTitle()
{
print("My job is (jobTitle)")
}
}

func:包含用于执行特定任务的代码块。

func addNumbers(num1:Int, num2:Int) -> Int  
{
return num1+num2
}

import:引入一个以独立单元构建的框架或者应用。

import UIKit

//可以使用 UIKit 框架下的所有代码
class Foo {}

init:类、结构体、枚举的实例的初始化准备过程。

class Person
{
init()
{
//设置默认值,实例准备被使用
}
}

inout:将一个值传入函数,并可以被函数修改,然后将值传回到调用处,来替换初始值。适用于引用类型和值类型。

func dangerousOp(_ error:inout NSError?)  
{
error = NSError(domain: "", code: 0, userInfo: ["":""])
}

var potentialError:NSError?
dangerousOp(&potentialError)

//代码运行到这里,potentialError 不再是 nil,而是已经被初始化

internal:访问控制权限,允许同一个模块下的所有源文件访问,如果在不同模块下则不允许访问。

class Person  
{
internal var jobTitle:String = ""
}

let aPerson = Person()
aPerson.jobTitle = "This can set anywhere in the application"

let:定义一个不可变的变量。

let constantString = "This cannot be mutated going forward"

open:访问控制权限,允许在定义的模块外也可以访问源文件里的所有类,并进行子类化。对于类成员,允许在定义的模块之外访问和重写。

open var foo:String? //这个属性允许在 app 内或 app 外重写和访问。在开发框架的时候,会应用到这个访问修饰符。

operator:特殊符号,用于检查、修改、组合值。

//一元运算符 "-",改变值的符号
let foo = 5
let anotherFoo = -foo //anotherFoo 等于 -5

//二元运算符 "+" 将两个值相加
let box = 5 + 3

//逻辑运算符 "&&" 将两个布尔值进行组合运算
if didPassCheckOne && didPassCheckTwo

//三元运算符需要使用三个值
let isLegalDrinkingAgeInUS:Bool = age >= 21 ? true : false

private:访问控制权限,只允许实体在定义的类以及相同源文件内的 extension 中访问。

class Person  
{
private var jobTitle:String = ""
}

// 当 extension 和 class 不在同一个源文件时
extension Person
{
// 无法编译通过,只有在同一个源文件下才可以访问
func printJobTitle()
{
print("My job is (jobTitle)")
}
}

protocol:定义了一组方法、属性或其它要求,用于满足特定任务和一系列功能。

protocol Blog  
{
var wordCount:Int { get set }
func printReaderStats()
}

class TTIDGPost : Blog
{
var wordCount:Int

init(wordCount:Int)
{
self.wordCount = wordCount
}

func printReaderStats()
{
//打印 post 的数据
}
}

public:访问控制权限,允许在定义的模块外也可以访问源文件里的所有类,但只有在同一个模块内可以进行子类化。对于类成员,允许在同个模块下访问和重写。

public var foo:String? //只允许在 app 内重写和访问。

static:用于定义类方法,在类型本身进行调用。此外还可以定义静态成员。

class Person  
{
var jobTitle:String?

static func assignRandomName(_ aPerson:Person)
{
aPerson.jobTitle = "Some random job"
}
}

let somePerson = Person()
Person.assignRandomName(somePerson)
//somePerson.jobTitle 的值是 "Some random job"

struct:通用、灵活的结构体,是程序的基础组成部分,并提供了默认初始化方法。与 class 不同,当 struct 在代码中被传递时,是被拷贝的,并不使用引用计数。除此之外,struct 没有下面的这些功能:

  • 使用继承。
  • 运行时的类型转换。
  • 使用析构方法。
struct Person  
{
var name:String
var age:Int
var gender:String
}

subscript:访问集合、列表、序列中成员元素的快捷方式。

var postMetrics = ["Likes":422, "ReadPercentage":0.58, "Views":3409]  
let postLikes = postMetrics["Likes"]

typealias:给代码中已经存在的类,取别名。

typealias JSONDictionary = [String: AnyObject]

func parseJSON(_ deserializedData:JSONDictionary){}

var:定义可变变量。

var mutableString = ""  
mutableString = "Mutated"

语句中的关键词

break:终止程序中循环的执行,比如 if 语句、switch 语句。

for idx in 0...3  
{
if idx % 2 == 0
{
//当 idx 等于偶数时,退出 for 循环
break
}
}

case:该语句在 switch 语句中列出,在每个分支可以进行模式匹配。

let box = 1

switch box
{
case 0:
print("Box equals 0")
case 1:
print("Box equals 1")
default:
print("Box doesn't equal 0 or 1")
}

continue:用于终止循环的当前迭代,并进入下一次迭代,而不会停止整个循环的执行。

for idx in 0...3  
{
if idx % 2 == 0
{
//直接开始循环的下一次迭代
continue
}

print("This code never fires on even numbers")
}

default:用于涵盖在 switch 语句中,所有未明确列出的枚举成员。

let box = 1

switch box
{
case 0:
print("Box equals 0")
case 1:
print("Box equals 1")
default:
print("Covers any scenario that doesn't get addressed above.")
}

defer:用于在程序离开当前作用域之前,执行一段代码。

func cleanUpIO()  
{
defer
{
print("This is called right before exiting scope")
}


//关闭文件流等。
}

do:用于表示处理错误代码段的开始。

do  
{
try expression
//语句
}
catch someError ex
{
//处理错误
}

else:与 if 语句结合使用。当条件为 true,执行一段代码。当条件为 false,执行另一段代码。

if val > 1  
{
print("val is greater than 1")
}
else
{
print("val is not greater than 1")
}

fallthrough:显式地允许从当前 case 跳转到下一个相邻 case 继续执行代码。

let box = 1

switch box
{
case 0:
print("Box equals 0")
fallthrough
case 1:
print("Box equals 0 or 1")
default:
print("Box doesn't equal 0 or 1")
}

for:在序列上迭代,比如一组特定范围内的数字,数组中的元素,字符串中的字符。*与关键字 in 成对使用。

for _ in 0..<3 { print ("This prints 3 times") }

guard:当有一个以上的条件不满足要求时,将离开当前作用域。同时还提供解包可选类型的功能。

private func printRecordFromLastName(userLastName: String?)
{
guard let name = userLastName, name != "Null" else
{
//userLastName = "Null",需要提前退出
return
}

//继续执行代码
print(dataStore.findByLastName(name))
}

if:当条件满足时,执行代码。

if 1 > 2  
{
print("This will never execute")
}

in:在序列上迭代,比如一组特定范围内的数字,数组中的元素,字符串中的字符。*与关键字 key 搭配使用。

for _ in 0..<3 { print ("This prints 3 times") }

repeat:在使用循环的判断条件之前,先执行一次循环中的代码。

repeat  
{
print("Always executes at least once before the condition is considered")
}
while 1 > 2

return:立刻终止当前上下文,离开当前作用域,此外在返回时可以额外携带一个值。

func doNothing()  
{
return //直接离开当前上下文

let anInt = 0
print("This never prints (anInt)")
}

func returnName() -> String?  
{
return self.userName //离开,并返回 userName 的值
}

switch:将给定的值与分支进行比较。执行第一个模式匹配成功的分支代码。

let box = 1

switch box
{
case 0:
print("Box equals 0")
fallthrough
case 1:
print("Box equals 0 or 1")
default:
print("Box doesn't equal 0 or 1")
}

where:要求关联类型必须遵守特定协议,或者类型参数和关联类型必须保持一致。也可以用于在 case 中提供额外条件,用于满足控制表达式。

where 从句可以应用于多种场景。以下例子指明了 where 的主要应用场景,泛型中的模式匹配。

protocol Nameable  
{
var name:String {get set}
}

func createdFormattedName(_ namedEntity:T) -> String where T:Equatable
{
//只有当实体同时遵守 Nameable 和 Equatable 协议的时候,才允许调用这个函数
return "This things name is " + namedEntity.name
}

for i in 03 where i % 2 == 0  
{
print(i) //打印 0 和 2
}

while:循环执行特定的一段语句,直到条件不满足时,停止循环。

while foo != bar  
{
print("Keeps going until the foo == bar")
}

表达式和类型中的关键字

Any:用于表示任意类型的实例,包括函数类型。

var anything = [Any]()

anything.append("Any Swift type can be added")
anything.append(0)
anything.append({(foo: String) -> String in "Passed in (foo)"})

as:类型转换运算符,用于尝试将值转成其它类型。

var anything = [Any]()

anything.append("Any Swift type can be added")
anything.append(0)
anything.append({(foo: String) -> String in "Passed in (foo)" })

let intInstance = anything[1] as? Int

或者

var anything = [Any]()

anything.append("Any Swift type can be added")
anything.append(0)
anything.append({(foo: String) -> String in "Passed in (foo)" })

for thing in anything
{
switch thing
{
case 0 as Int:
print("It's zero and an Int type")
case let someInt as Int:
print("It's an Int that's not zero but (someInt)")
default:
print("Who knows what it is")
}
}

catch:如果在 do 中抛出一个错误,catch 会尝试进行匹配,并决定如何处理错误。*我写的一篇 Swift 错误处理的博客节选

do  
{
try haveAWeekend(4)
}
catch WeekendError.Overtime(let hoursWorked)
{
print("You worked (hoursWorked) more than you should have")
}
catch WeekendError.WorkAllWeekend
{
print("You worked 48 hours :-0")
}
catch
{
print("Gulping the weekend exception")
}

false:Swift 用于表示布尔值的两个常量值之一,true 的相反值。

let alwaysFalse = false  
let alwaysTrue = true

if alwaysFalse { print("Won't print, alwaysFalse is false 😉")}

is:类型检查运算符,用于确定实例是否为某个子类类型。

class Person {}  
class Programmer : Person {}
class Nurse : Person {}

let people = [Programmer(), Nurse()]

for aPerson in people
{
if aPerson is Programmer
{
print("This person is a dev")
}
else if aPerson is Nurse
{
print("This person is a nurse")
}
}

nil:在 Swift 中表示任意类型的无状态值。

与 Objective-C 中的 nil 不同,Objective-C 中的 nil 表示指向不存在对象的指针。

class Person{}  
struct Place{}

//任何 Swift 类型或实例可以为 nil
var statelessPerson:Person? = nil
var statelessPlace:Place? = nil
var statelessInt:Int? = nil
var statelessString:String? = nil

rethrows:指明当前函数只有当参数抛出 error 时,才会抛出 error。

func networkCall(onComplete:() throws -> Void) rethrows  
{
do
{
try onComplete()
}
catch
{
throw SomeError.error
}
}

super:在子类中,暴露父类的方法、属性、下标。

class Person  
{
func printName()
{
print("Printing a name. ")
}
}

class Programmer : Person
{
override func printName()
{
super.printName()
print("Hello World!")
}
}

let aDev = Programmer()
aDev.printName() //打印 Printing a name. Hello World!

self:任何类型的实例都拥有的隐式属性,等同于实例本身。此外还可以用于区分函数参数和成员属性名称相同的情况。

class Person  
{
func printSelf()
{
print("This is me: (self)")
}
}

let aPerson = Person()
aPerson.printSelf() //打印 "This is me: Person"

Self:在协议中,表示遵守当前协议的实体类型。

protocol Printable  
{
func printTypeTwice(otherMe:Self)
}

struct Foo : Printable
{
func printTypeTwice(otherMe: Foo)
{
print("I am me plus (otherMe)")
}
}

let aFoo = Foo()
let anotherFoo = Foo()

aFoo.printTypeTwice(otherMe: anotherFoo) //打印 I am me plus Foo()

throw:用于在当前上下文,显式抛出 error。

enum WeekendError: Error  
{
case Overtime
case WorkAllWeekend
}

func workOvertime () throws
{
throw WeekendError.Overtime
}

throws:指明在一个函数、方法、初始化方法中可能会抛出 error。

enum WeekendError: Error  
{
case Overtime
case WorkAllWeekend
}

func workOvertime () throws
{
throw WeekendError.Overtime
}

//"throws" 表明在调用方法时,需要使用 try,try?,try!
try workOvertime()

true:Swift 用于表示布尔值的两个常量值之一,表示为真。

let alwaysFalse = false  
let alwaysTrue = true

if alwaysTrue { print("Always prints")}

try:表明接着调用的函数可能会抛出 error。有三种不同的使用方式:try,try?, try!。

let aResult = try dangerousFunction() //处理 error,或者继续传递 error  
let aResult = try! dangerousFunction() //程序可能会闪退
if let aResult = try? dangerousFunction() //解包可选类型。

模式中的关键字

_:用于匹配或省略任意值的通配符。

for _ in 0..<3  
{
print("Just loop 3 times, index has no meaning")
}

另外一种用法:

let _ = Singleton() //忽略不使用的变量

以#开头的关键字

#available:基于平台参数,通过 ifwhileguard 语句的条件,在运行时检查 API 的可用性。

if #available(iOS 10, *)  
{
print("iOS 10 APIs are available")
}

#colorLiteral:在 playground 中使用的字面表达式,用于创建颜色选取器,选取后赋值给变量。

let aColor = #colorLiteral //创建颜色选取器

#column:一种特殊的字面量表达式,用于获取字面量表示式的起始列数。

class Person  
{
func printInfo()
{
print("Some person info - on column (#column)")
}
}

let aPerson = Person()
aPerson.printInfo() //Some person info - on column 53

#else:条件编译控制语句,用于控制程序在不同条件下执行不同代码。与 #if 语句结合使用。当条件为 true,执行对应代码。当条件为 false,执行另一段代码。

#if os(iOS)  
print("Compiled for an iOS device")
#else
print("Not on an iOS device")
#endif

#elseif:条件编译控制语句,用于控制程序在不同条件下执行代码。与 #if 语句结合使用。当条件为 true,执行对应代码。

#if os(iOS)  
print("Compiled for an iOS device")
#elseif os(macOS)
print("Compiled on a mac computer")
#endif

#endif:条件编译控制语句,用于控制程序在不同条件下执行代码。用于表明条件编译代码的结尾。

#if os(iOS)  
print("Compiled for an iOS device")
#endif

#file:特殊字面量表达式,返回当前代码所在源文件的名称。

class Person  
{
func printInfo()
{
print("Some person info - inside file (#file)")
}
}

let aPerson = Person()
aPerson.printInfo() //Some person info - inside file /*代码所在 playground 文件路径*/

#fileReference:playground 字面量语法,用于创建文件选取器,选取并返回 NSURL 实例。

let fontFilePath = #fileReference //创建文件选取器

#function:特殊字面量表达式,返回函数名称。在方法中,返回方法名。在属性的 getter 或者 setter 中,返回属性名。在特殊的成员中,比如 init 或 subscript 中,返回关键字名称。在文件的最顶层时,返回当前所在模块名称。

class Person  
{
func printInfo()
{
print("Some person info - inside function (#function)")
}
}

let aPerson = Person()
aPerson.printInfo() //Some person info - inside function printInfo()

#if:条件编译控制语句,用于控制程序在不同条件下编译代码。通过判断条件,决定是否执行代码。

#if os(iOS)  
print("Compiled for an iOS device")
#endif

#imageLiteral:playground 字面量语法,创建图片选取器,选择并返回 UIImage 实例。

let anImage = #imageLiteral //在 playground 文件中选取图片

#line:特殊字面量表达式,用于获取当前代码的行数。

class Person  
{
func printInfo()
{
print("Some person info - on line number (#line)")
}
}

let aPerson = Person()
aPerson.printInfo() //Some person info - on line number 5

#selector:用于创建 Objective-C selector 的表达式,可以静态检查方法是否存在,并暴露给 Objective-C。

//静态检查,确保 doAnObjCMethod 方法存在  
control.sendAction(#selector(doAnObjCMethod), to: target, forEvent: event)

#sourceLocation:行控制语句,可以指定与原先完全不同的行数和源文件名。通常在 Swift 诊断、debug 时使用。

#sourceLocation(file:"foo.swift", line:6)

//打印新值
print(#file)
print(#line)

//重置行数和文件名
#sourceLocation()

print(#file)
print(#line)

特定上下文中的关键字

这些关键字,在处于对应上下文之外时,可以用作标识符。

associativity:指明同一优先级的运算符,在缺少大括号的情况,按什么顺序结合。使用 leftrightnone

infix operator ~ { associativity right precedence 140 }  
4 ~ 8

convenience:次等的便利构造器,最后会调用指定构造器初始化实例。

class Person  
{
var name:String

init(_ name:String)
{
self.name = name
}

convenience init()
{
self.init("No Name")
}
}

let me = Person()
print(me.name)//打印 "No Name"

dynamic:指明编译器不会对类成员或者函数的方法进行内联或虚拟化。这意味着对这个成员的访问是使用 Objective-C 运行时进行动态派发的(代替静态调用)。

class Person  
{
//隐式指明含有 "objc" 属性
//这对依赖于 Objc-C 黑魔法的库或者框架非常有用
//比如 KVO、KVC、Swizzling
dynamic var name:String?
}

didSet:属性观察者,当值存储到属性后马上调用。

var data = [1,2,3]  
{
didSet
{
tableView.reloadData()
}
}

final:防止方法、属性、下标被重写。

final class Person {}  
class Programmer : Person {} //编译错误

get:返回成员的值。还可以用在计算型属性上,间接获取其它属性的值。

class Person  
{
var name:String
{
get { return self.name }
set { self.name = newValue}
}

var indirectSetName:String
{
get
{
if let aFullTitle = self.fullTitle
{
return aFullTitle
}
return ""
}

set (newTitle)
{
//如果没有定义 newTitle,可以使用 newValue
self.fullTitle = "(self.name) :(newTitle)"
}
}
}

infix:指明一个用于两个值之间的运算符。如果一个全新的全局运算符被定义为 infix,还需要指定优先级。

let twoIntsAdded = 2 + 3

indirect:指明在枚举类型中,存在成员使用相同枚举类型的实例作为关联值的情况。

indirect enum Entertainment  
{
case eventType(String)
case oneEvent(Entertainment)
case twoEvents(Entertainment, Entertainment)
}

let dinner = Entertainment.eventType("Dinner")
let movie = Entertainment.eventType("Movie")

let dateNight = Entertainment.twoEvents(dinner, movie)

lazy:指明属性的初始值,直到第一次被使用时,才进行初始化。

class Person  
{
lazy var personalityTraits = {
//昂贵的数据库开销
return ["Nice", "Funny"]
}()
}
let aPerson = Person()
aPerson.personalityTraits //当 personalityTraits 首次被访问时,数据库才开始工作

left:指明运算符的结合性是从左到右。在没有使用大括号时,可以用于正确判断同一优先级运算符的执行顺序。

//"-" 运算符的结合性是从左到右
10-2-4 //根据结合性,可以看做 (10-2) - 4

mutating:允许在方法中修改结构体或者枚举实例的属性值。

struct Person  
{
var job = ""

mutating func assignJob(newJob:String)
{
self = Person(job: newJob)
}
}

var aPerson = Person()
aPerson.job //""

aPerson.assignJob(newJob: "iOS Engineer at Buffer")
aPerson.job //iOS Engineer at Buffer

none:是一个没有结合性的运算符。不允许这样的运算符相邻出现。

//"<" 是非结合性的运算符
1 < 2 < 3 //编译失败

nonmutating:指明成员的 setter 方法不会修改实例的值,但可能会有其它后果。

enum Paygrade  
{
case Junior, Middle, Senior, Master

var experiencePay:String?
{
get
{
database.payForGrade(String(describing:self))
}

nonmutating set
{
if let newPay = newValue
{
database.editPayForGrade(String(describing:self), newSalary:newPay)
}
}
}
}

let currentPay = Paygrade.Middle

//将 Middle pay 更新为 45k, 但不会修改 experiencePay 值
currentPay.experiencePay = "$45,000"

optional:用于指明协议中的可选方法。遵守该协议的实体类可以不实现这个方法。

@objc protocol Foo  
{
func requiredFunction()
@objc optional func optionalFunction()
}

class Person : Foo
{
func requiredFunction()
{
print("Conformance is now valid")
}
}

override:指明子类会提供自定义实现,覆盖父类的实例方法、类型方法、实例属性、类型属性、下标。如果没有实现,则会直接继承自父类。

class Person  
{
func printInfo()
{
print("I'm just a person!")
}
}

class Programmer : Person
{
override func printInfo()
{
print("I'm a person who is a dev!")
}
}

let aPerson = Person()
let aDev = Programmer()

aPerson.printInfo() //打印 I'm just a person!
aDev.printInfo() //打印 I'm a person who is a dev!

postfix:位于值后面的运算符。

var optionalStr:String? = "Optional"  
print(optionalStr!)

precedence:指明某个运算符的优先级高于别的运算符,从而被优先使用。

infix operator ~ { associativity right precedence 140 }  
4 ~ 8

prefix:位于值前面的运算符。

var anInt = 2  
anInt = -anInt //anInt 等于 -2

required:确保编译器会检查该类的所有子类,全部实现了指定的构造器方法。

class Person  
{
var name:String?

required init(_ name:String)
{
self.name = name
}
}

class Programmer : Person
{
//如果不实现这个方法,编译不会通过
required init(_ name: String)
{
super.init(name)
}
}

right:指明运算符的结合性是从右到左的。在没有使用大括号时,可以用于正确判断同一优先级运算符的顺序。

//"??" 运算符结合性是从右到左
var box:Int?
var sol:Int? = 2

let foo:Int = box ?? sol ?? 0 //Foo 等于 2

set:通过获取的新值来设置成员的值。同样可以用于计算型属性来间接设置其它属性。如果计算型属性的 setter 没有定义新值的名称,可以使用默认的 newValue。

class Person  
{
var name:String
{
get { return self.name }
set { self.name = newValue}
}

var indirectSetName:String
{
get
{
if let aFullTitle = self.fullTitle
{
return aFullTitle
}
return ""
}

set (newTitle)
{
//如果没有定义 newTitle,可以使用 newValue
self.fullTitle = "(self.name) :(newTitle)"
}
}
}

Type:表示任意类型的类型,包括类类型、结构类型、枚举类型、协议类型。

class Person {}  
class Programmer : Person {}

let aDev:Programmer.Type = Programmer.self

unowned:让循环引用中的实例 A 不要强引用实例 B。前提条件是实例 B 的生命周期要长于 A 实例。

class Person  
{
var occupation:Job?
}

//当 Person 实例不存在时,job 也不会存在。job 的生命周期取决于持有它的 Person。
class Job
{
unowned let employee:Person

init(with employee:Person)
{
self.employee = employee
}
}

weak:允许循环引用中的实例 A 弱引用实例 B ,而不是强引用。实例 B 的生命周期更短,并会被先释放。

class Person  
{
var residence:House?
}

class House
{
weak var occupant:Person?
}

var me:Person? = Person()
var myHome:House? = House()

me!.residence = myHome
myHome!.occupant = me

me = nil
myHome!.occupant // myHome 等于 nil

willSet:属性观察者,在值存储到属性之前调用。

class Person  
{
var name:String?
{
willSet(newValue) {print("I've got a new name, it's (newValue)!")}
}
}

let aPerson = Person()
aPerson.name = "Jordan" //在赋值之前,打印 "I've got a new name, it's Jordan!"

总结

哇噢!

这真是一次有趣的创作。我学会了好多在写之前没想到的东西。但我认为这里的诀窍并不是要把它记住,而是把它当做一份可以用于测验的定义清单。

相反地,我建议你把这份清单放在手边,并时不时地回顾一下。如果你能这样做的话,下一次在不同场景下需要使用特定的关键字,你肯定就能马上回想起来并使用它啦。

下回再见咯。

本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 http://swift.gg

给 UIView 来点烟花

作者:Tomasz Szulc,原文链接,原文日期:2018-09
译者:Joeytat;校对:numbbbbbWAMaker;定稿:Pancf

你也很喜欢常用 app 里的那些小细节吧?当我从 dribbble 中寻找灵感时,就发现了这个漂亮的设计:当用户在某个重要的视图中修改设置或者进行了什么操作时,会有烟花在周围绽放。于是我就在想这个东西有多难实现,然后过了一段时间,我完成了 :)

hero

烟花的细节

下面是对于这个效果的详细描述。烟花应该在视图周围的某个特殊的位置爆开,可能是按钮在点击事件响应时。当点击发生时,烟花应该在按钮的四角爆开,并且爆炸产生的火花应该按照自身的轨迹移动。

final

超喜欢这个效果! 不仅让我感受到视觉上的愉悦,还让我想要不停地戳这个按钮! :) 🎉

现在让我们再看一眼这个动画。每次生成的烟花,其整体行为是大致相似的。但还是在火花的轨迹和大小上有一些区别。让我们拆开来说。

  • 每一次点击都会产生两处烟花
  • 每一处烟花会产生 8 个火花
  • 每个火花都遵循着自己的轨迹
  • 轨迹看起来相似,但其实不完全一样。从爆炸开始的位置来看,有部分朝,有部分朝,剩余的朝

火花的分布

这个烟花特效有着简单的火花分布规则。将爆炸点分为四块「视线区域」来看:上左,上右,下左,下右,每个区域都有两个火花。

sparks distribution

火花的轨迹

火花的移动有着自己的轨迹。在一处烟花中有 8 个火花,那至少需要 8 道轨迹。理想状态下应该有更多的轨迹,可以增加一些随机性,这样连续爆发烟花的时候,不会看起来和前一个完全一样。

spark-trajectories

我为每一个区域创建了 4 条轨迹,这样就赋予了两倍于火花数量的随机性。为了方便计算,我统一了每条轨迹的初始点。因为我用了不同的工具来可视化这些轨迹,所以图上的轨迹和我完成的效果略有不同 - 但你能明白我的想法就行 :)

_实现_

理论足够了。接下来让我们把各个模块拼凑起来。

protocol SparkTrajectory {

/// 存储着定义轨迹所需要的所有的点
var points: [CGPoint] { get set }

/// 用 path 来表现轨迹
var path: UIBezierPath { get }
}

这是一个用于表示火花轨迹的协议。为了能够更简单地创建各式各样的轨迹,我定义了这个通用接口协议,并且选择基于三阶 贝塞尔曲线 来实现轨迹;还添加了一个 init 方法,这样我就可以通过一行代码来创建轨迹了。三阶贝塞尔曲线必须包含四个点。第一个和最后一个点定义了轨迹的开始和结束的位置,中间的两个点用于控制曲线的弯曲度。你可以用在线数学工具 desmos 来调整自己的贝塞尔曲线。

/// 拥有两个控制点的贝塞尔曲线
struct CubicBezierTrajectory: SparkTrajectory {

var points = [CGPoint]()

init(_ x0: CGFloat, _ y0: CGFloat,
_ x1: CGFloat, _ y1: CGFloat,
_ x2: CGFloat, _ y2: CGFloat,
_ x3: CGFloat, _ y3: CGFloat) {
self.points.append(CGPoint(x: x0, y: y0))
self.points.append(CGPoint(x: x1, y: y1))
self.points.append(CGPoint(x: x2, y: y2))
self.points.append(CGPoint(x: x3, y: y3))
}

var path: UIBezierPath {
guard self.points.count == 4 else { fatalError("4 points required") }

let path = UIBezierPath()
path.move(to: self.points[0])
path.addCurve(to: self.points[3], controlPoint1: self.points[1], controlPoint2: self.points[2])
return path
}
}

desmos-tool

接下来要实现的是一个能够创建随机轨迹的工厂。前面的图中你可以看到轨迹是根据颜色来分组的。我只创建了上右和下右两块位置的轨迹,然后进行了镜像复制。这对于我们将要发射的烟花来说已经足够了🚀

protocol SparkTrajectoryFactory {}

protocol ClassicSparkTrajectoryFactoryProtocol: SparkTrajectoryFactory {

func randomTopRight() -> SparkTrajectory
func randomBottomRight() -> SparkTrajectory
}

final class ClassicSparkTrajectoryFactory: ClassicSparkTrajectoryFactoryProtocol {

private lazy var topRight: [SparkTrajectory] = {
return [
CubicBezierTrajectory(0.00, 0.00, 0.31, -0.46, 0.74, -0.29, 0.99, 0.12),
CubicBezierTrajectory(0.00, 0.00, 0.31, -0.46, 0.62, -0.49, 0.88, -0.19),
CubicBezierTrajectory(0.00, 0.00, 0.10, -0.54, 0.44, -0.53, 0.66, -0.30),
CubicBezierTrajectory(0.00, 0.00, 0.19, -0.46, 0.41, -0.53, 0.65, -0.45),
]
}()

private lazy var bottomRight: [SparkTrajectory] = {
return [
CubicBezierTrajectory(0.00, 0.00, 0.42, -0.01, 0.68, 0.11, 0.87, 0.44),
CubicBezierTrajectory(0.00, 0.00, 0.35, 0.00, 0.55, 0.12, 0.62, 0.45),
CubicBezierTrajectory(0.00, 0.00, 0.21, 0.05, 0.31, 0.19, 0.32, 0.45),
CubicBezierTrajectory(0.00, 0.00, 0.18, 0.00, 0.31, 0.11, 0.35, 0.25),
]
}()

func randomTopRight() -> SparkTrajectory {
return self.topRight[Int(arc4random_uniform(UInt32(self.topRight.count)))]
}

func randomBottomRight() -> SparkTrajectory {
return self.bottomRight[Int(arc4random_uniform(UInt32(self.bottomRight.count)))]
}
}

这里先创建了用来表示火花轨迹工厂的抽象协议,还有一个我将其命名为经典烟花的火花轨迹的抽象协议,这样的抽象可以方便后续将其替换成其他的轨迹协议。

如同我前面提到的,我通过 desmos 创建了两组轨迹,对应着右上,和右下两块区域。

重要提醒:如果在 desmos 上 y 轴所显示的是正数,那么你应该将其转换成负数。因为在 iOS 系统中,越接近屏幕顶部 y 轴的值越小,所以 y 轴的值需要翻转一下。

并且值得一提的是,为了后面好计算,所有的轨迹初始点都是 (0,0)。

我们现在创建好了轨迹。接下来创建一些视图来表示火花。对于经典烟花来说,只需要有颜色的圆圈就行。通过抽象可以让我们在未来以更低的成本,创建不同的火花视图。比如小鸭子图片,或者是胖吉猫 :)

class SparkView: UIView {}

final class CircleColorSparkView: SparkView {

init(color: UIColor, size: CGSize) {
super.init(frame: CGRect(origin: .zero, size: size))
self.backgroundColor = color
self.layer.cornerRadius = self.frame.width / 2.0
}

required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}

extension UIColor {

static var sparkColorSet1: [UIColor] = {
return [
UIColor(red:0.89, green:0.58, blue:0.70, alpha:1.00),
UIColor(red:0.96, green:0.87, blue:0.62, alpha:1.00),
UIColor(red:0.67, green:0.82, blue:0.94, alpha:1.00),
UIColor(red:0.54, green:0.56, blue:0.94, alpha:1.00),
]
}()
}

为了创建火花视图,我们还需要一个工厂数据以填充,需要的数据是火花的大小,以及用来决定火花在哪个烟花的索引(用于增加随机性)。

protocol SparkViewFactoryData {

var size: CGSize { get }
var index: Int { get }
}

protocol SparkViewFactory {

func create(with data: SparkViewFactoryData) -> SparkView
}

class CircleColorSparkViewFactory: SparkViewFactory {

var colors: [UIColor] {
return UIColor.sparkColorSet1
}

func create(with data: SparkViewFactoryData) -> SparkView {
let color = self.colors[data.index % self.colors.count]
return CircleColorSparkView(color: color, size: data.size)
}
}

你看这样抽象了之后,就算再实现一个像胖吉猫的火花也会很简单。接下来让我们来创建经典烟花

typealias FireworkSpark = (sparkView: SparkView, trajectory: SparkTrajectory)

protocol Firework {

/// 烟花的初始位置
var origin: CGPoint { get set }

/// 定义了轨迹的大小. 轨迹都是统一大小
/// 所以需要在展示到屏幕上前将其放大
var scale: CGFloat { get set }

/// 火花的大小
var sparkSize: CGSize { get set }

/// 获取轨迹
var trajectoryFactory: SparkTrajectoryFactory { get }

/// 获取火花视图
var sparkViewFactory: SparkViewFactory { get }

func sparkViewFactoryData(at index: Int) -> SparkViewFactoryData
func sparkView(at index: Int) -> SparkView
func trajectory(at index: Int) -> SparkTrajectory
}

extension Firework {

/// 帮助方法,用于返回火花视图及对应的轨迹
func spark(at index: Int) -> FireworkSpark {
return FireworkSpark(self.sparkView(at: index), self.trajectory(at: index))
}
}

这就是烟花的抽象。为了表示一个烟花需要这些东西:

  • origin
  • scale
  • sparkSize
  • trajectoryFactory
  • sparkViewFactory

在我们实现协议之前,还有一个我之前没有提到过的叫做按轨迹缩放的概念。当火花处于轨迹 <-1, 1> 或相似的位置时,我们希望它的大小会跟随轨迹变化。我们还需要放大路径以覆盖更大的屏幕显示效果。此外,我们还需要支持水平翻转路径,以方便我们实现经典烟花左侧部分的轨迹,并且还要让轨迹能朝某个指定方向偏移一点(增加随机性)。下面是两个能够帮助我们达到目的的方法,我相信这段代码已经不需要更多描述了。

extension SparkTrajectory {

/// 缩放轨迹使其符合各种 UI 的要求
/// 在各种形变和 shift: 之前使用
func scale(by value: CGFloat) -> SparkTrajectory {
var copy = self
(0..<self.points.count).forEach { copy.points[$0].multiply(by: value) }
return copy
}

/// 水平翻转轨迹
func flip() -> SparkTrajectory {
var copy = self
(0..<self.points.count).forEach { copy.points[$0].x *= -1 }
return copy
}

/// 偏移轨迹,在每个点上生效
/// 在各种形变和 scale: 和之后使用
func shift(to point: CGPoint) -> SparkTrajectory {
var copy = self
let vector = CGVector(dx: point.x, dy: point.y)
(0..<self.points.count).forEach { copy.points[$0].add(vector: vector) }
return copy
}
}

好了,接下来就是实现经典烟花。

class ClassicFirework: Firework {

/**
x | x
x | x
|
---------------
x | x
x |
| x
**/

private struct FlipOptions: OptionSet {

let rawValue: Int

static let horizontally = FlipOptions(rawValue: 1 << 0)
static let vertically = FlipOptions(rawValue: 1 << 1)
}

private enum Quarter {

case topRight
case bottomRight
case bottomLeft
case topLeft
}

var origin: CGPoint
var scale: CGFloat
var sparkSize: CGSize

var maxChangeValue: Int {
return 10
}

var trajectoryFactory: SparkTrajectoryFactory {
return ClassicSparkTrajectoryFactory()
}

var classicTrajectoryFactory: ClassicSparkTrajectoryFactoryProtocol {
return self.trajectoryFactory as! ClassicSparkTrajectoryFactoryProtocol
}

var sparkViewFactory: SparkViewFactory {
return CircleColorSparkViewFactory()
}

private var quarters = [Quarter]()

init(origin: CGPoint, sparkSize: CGSize, scale: CGFloat) {
self.origin = origin
self.scale = scale
self.sparkSize = sparkSize
self.quarters = self.shuffledQuarters()
}

func sparkViewFactoryData(at index: Int) -> SparkViewFactoryData {
return DefaultSparkViewFactoryData(size: self.sparkSize, index: index)
}

func sparkView(at index: Int) -> SparkView {
return self.sparkViewFactory.create(with: self.sparkViewFactoryData(at: index))
}

func trajectory(at index: Int) -> SparkTrajectory {
let quarter = self.quarters[index]
let flipOptions = self.flipOptions(for: quarter)
let changeVector = self.randomChangeVector(flipOptions: flipOptions, maxValue: self.maxChangeValue)
let sparkOrigin = self.origin.adding(vector: changeVector)
return self.randomTrajectory(flipOptions: flipOptions).scale(by: self.scale).shift(to: sparkOrigin)
}

private func flipOptions(`for` quarter: Quarter) -> FlipOptions {
var flipOptions: FlipOptions = []
if quarter == .bottomLeft || quarter == .topLeft {
flipOptions.insert(.horizontally)
}

if quarter == .bottomLeft || quarter == .bottomRight {
flipOptions.insert(.vertically)
}

return flipOptions
}

private func shuffledQuarters() -> [Quarter] {
var quarters: [Quarter] = [
.topRight, .topRight,
.bottomRight, .bottomRight,
.bottomLeft, .bottomLeft,
.topLeft, .topLeft
]

var shuffled = [Quarter]()
for _ in 0..<quarters.count {
let idx = Int(arc4random_uniform(UInt32(quarters.count)))
shuffled.append(quarters[idx])
quarters.remove(at: idx)
}

return shuffled
}

private func randomTrajectory(flipOptions: FlipOptions) -> SparkTrajectory {
var trajectory: SparkTrajectory

if flipOptions.contains(.vertically) {
trajectory = self.classicTrajectoryFactory.randomBottomRight()
} else {
trajectory = self.classicTrajectoryFactory.randomTopRight()
}

return flipOptions.contains(.horizontally) ? trajectory.flip() : trajectory
}

private func randomChangeVector(flipOptions: FlipOptions, maxValue: Int) -> CGVector {
let values = (self.randomChange(maxValue), self.randomChange(maxValue))
let changeX = flipOptions.contains(.horizontally) ? -values.0 : values.0
let changeY = flipOptions.contains(.vertically) ? values.1 : -values.0
return CGVector(dx: changeX, dy: changeY)
}

private func randomChange(_ maxValue: Int) -> CGFloat {
return CGFloat(arc4random_uniform(UInt32(maxValue)))
}
}

大多数代码都是 Firework 协议的实现,所以应该很容易理解。我们在各处传递了需要的工厂类,还添加了一个额外的枚举类型来随机地为每个火花指定轨迹。

有少数几个方法用来为烟花和火花增加随机性。

还引入了一个 quarters 属性,其中包含了火花的所有的方位。我们通过 shuffledQuarters: 来重新排列,以确保我们不会总是在相同的方位创建相同数量的火花。

好了,我们创建好了烟花,接下来怎么让火花动起来呢?这就引入了火花动画启动器的概念。

protocol SparkViewAnimator {

func animate(spark: FireworkSpark, duration: TimeInterval)
}

这个方法接受一个包含火花视图和其对应轨迹的元组 FireworkSpark,以及动画的持续时间。方法的实现取决于我们。我自己的实现蛮多的,但主要做了三件事情:让火花视图跟随轨迹,同时缩放火花(带有随机性),修改其不透明度。简单吧。同时得益于 SparkViewAnimator 的抽象度,我们还可以很简单地将其替换成任何我们想要的动画效果。

struct ClassicFireworkAnimator: SparkViewAnimator {

func animate(spark: FireworkSpark, duration: TimeInterval) {
spark.sparkView.isHidden = false // show previously hidden spark view

CATransaction.begin()

// 火花的位置
let positionAnim = CAKeyframeAnimation(keyPath: "position")
positionAnim.path = spark.trajectory.path.cgPath
positionAnim.calculationMode = kCAAnimationLinear
positionAnim.rotationMode = kCAAnimationRotateAuto
positionAnim.duration = duration

// 火花的缩放
let randomMaxScale = 1.0 + CGFloat(arc4random_uniform(7)) / 10.0
let randomMinScale = 0.5 + CGFloat(arc4random_uniform(3)) / 10.0

let fromTransform = CATransform3DIdentity
let byTransform = CATransform3DScale(fromTransform, randomMaxScale, randomMaxScale, randomMaxScale)
let toTransform = CATransform3DScale(CATransform3DIdentity, randomMinScale, randomMinScale, randomMinScale)
let transformAnim = CAKeyframeAnimation(keyPath: "transform")

transformAnim.values = [
NSValue(caTransform3D: fromTransform),
NSValue(caTransform3D: byTransform),
NSValue(caTransform3D: toTransform)
]

transformAnim.duration = duration
transformAnim.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
spark.sparkView.layer.transform = toTransform

// 火花的不透明度
let opacityAnim = CAKeyframeAnimation(keyPath: "opacity")
opacityAnim.values = [1.0, 0.0]
opacityAnim.keyTimes = [0.95, 0.98]
opacityAnim.duration = duration
spark.sparkView.layer.opacity = 0.0

// 组合动画
let groupAnimation = CAAnimationGroup()
groupAnimation.animations = [positionAnim, transformAnim, opacityAnim]
groupAnimation.duration = duration

CATransaction.setCompletionBlock({
spark.sparkView.removeFromSuperview()
})

spark.sparkView.layer.add(groupAnimation, forKey: "spark-animation")

CATransaction.commit()
}
}

现在的代码已经足够让我们在特定的视图上展示烟花了。我又更进了一步,创建了一个 ClassicFireworkController 来处理所有的工作,这样用一行代码就能启动烟花。

这个烟花控制器还做了另一件事。它可以修改烟花的 zPosition,这样我们可以让烟花一前一后地展示,效果更好看一些。

class ClassicFireworkController {

var sparkAnimator: SparkViewAnimator {
return ClassicFireworkAnimator()
}

func createFirework(at origin: CGPoint, sparkSize: CGSize, scale: CGFloat) -> Firework {
return ClassicFirework(origin: origin, sparkSize: sparkSize, scale: scale)
}

/// 让烟花在其源视图的角落附近爆开
func addFireworks(count fireworksCount: Int = 1,
sparks sparksCount: Int,
around sourceView: UIView,
sparkSize: CGSize = CGSize(width: 7, height: 7),
scale: CGFloat = 45.0,
maxVectorChange: CGFloat = 15.0,
animationDuration: TimeInterval = 0.4,
canChangeZIndex: Bool = true) {
guard let superview = sourceView.superview else { fatalError() }

let origins = [
CGPoint(x: sourceView.frame.minX, y: sourceView.frame.minY),
CGPoint(x: sourceView.frame.maxX, y: sourceView.frame.minY),
CGPoint(x: sourceView.frame.minX, y: sourceView.frame.maxY),
CGPoint(x: sourceView.frame.maxX, y: sourceView.frame.maxY),
]

for _ in 0..<fireworksCount {
let idx = Int(arc4random_uniform(UInt32(origins.count)))
let origin = origins[idx].adding(vector: self.randomChangeVector(max: maxVectorChange))

let firework = self.createFirework(at: origin, sparkSize: sparkSize, scale: scale)

for sparkIndex in 0..<sparksCount {
let spark = firework.spark(at: sparkIndex)
spark.sparkView.isHidden = true
superview.addSubview(spark.sparkView)

if canChangeZIndex {
let zIndexChange: CGFloat = arc4random_uniform(2) == 0 ? -1 : +1
spark.sparkView.layer.zPosition = sourceView.layer.zPosition + zIndexChange
} else {
spark.sparkView.layer.zPosition = sourceView.layer.zPosition
}

self.sparkAnimator.animate(spark: spark, duration: animationDuration)
}
}
}

private func randomChangeVector(max: CGFloat) -> CGVector {
return CGVector(dx: self.randomChange(max: max), dy: self.randomChange(max: max))
}

private func randomChange(max: CGFloat) -> CGFloat {
return CGFloat(arc4random_uniform(UInt32(max))) - (max / 2.0)
}
}

这个控制器只做了几件事情。随机选择了一个角落展示烟花。在烟花出现的位置,烟花和火花的数量上增加了一些随机性。然后将火花添加到目标视图上,如果需要的话还会调整 zIndex,最后启动了动画。

几乎所有的参数都设置了默认参数,所以你可以不管他们。直接通过你的控制器调用这个:

self.fireworkController.addFireworks(count: 2, sparks: 8, around: button)

然后,哇!

classic

从这一步起,新添加一个像下面这样的烟花就变得非常简单了。你只需要定义新的轨迹,创建一个新的烟花,并且按照你希望的样子来实现即可。将这些代码放入一个控制器可以让你想在哪里启动烟花都很简单 :) 或者你也可以直接使用这个喷泉烟花,我已经把它放在了我的 github 项目 tomkowz/fireworks 中。

fountain

_总结_

这个动画效果的实现并不简单但也不算很难。通过对问题(在我们的情况下是动画效果)的正确分析,我们可以将其分解成多个小问题,逐个解决然后将其组合在一起。真希望我有机会能够在未来的的项目中使用这个效果🎉

好啦这就是今天的内容。感谢阅读!

本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 http://swift.gg

Bundles and Packages

作者:Mattt,原文链接,原文日期:2018-12-17
译者:WAMaker;校对:numbbbbbBigNerdCoding;定稿:Forelax

在这个给予的季节,让我们停下脚步,思考一个现代计算机系统赐予我们的最棒的礼物:抽象。

在数百万 CPU 晶体管、SSD 扇区和 LCD 像素共同协作下,全球数十亿人能够日常使用计算机和移动设备而对此全然不知。这一切都应归功于像文件,目录,应用和文档这样的抽象。

这周的 NSHipster,我们将讨论苹果平台上两个重要的抽象:包与包裹。🎁


尽管是不同的概念,包与包裹这两个术语经常会被替换使用。毫无疑问,造成困惑的部分原因出自它们相似的名称,但或许主要原因是许多包恰好也是包裹(反之亦然)。

在我们深入之前,先定义一下这两个术语:

  • 包是指具有已知结构的,包含可执行代码,以及代码所需的资源的目录。
  • 包裹是指在访达中看起来像是文件的目录。

下图展示了包与包裹之间的关系,将应用、框架包、插件包和文档分别放入一个或多个分类之中:
diagram

如果对两者的区别你依然感到困惑,这个类比或许能帮助你理解:
把包裹想象成是一个内容被隐藏的盒子(📦),作为一个独立的实体而存在。这点与包不同,包更像是一个背包(🎒) —— 每一款都有特殊的口袋和隔层用来携带你需要的东西,不同的配置用以决定是带去学校,去工作,还是去健身房。如果某样东西既是包也是包裹,恰似行李(🧳)一般:像盒子一样浑然一体,像背包一样分隔自如。

包(Bundles)

包为代码和资源的组织提供了特定结构,意在提升开发者的体验。这个结构不仅允许预测性的加载代码和资源,同时也支持类似于本地化这样的系统性特性。

包分属于以下三个类别,每一种都有它自己特殊的结构和要求:

  • 应用包(App Bundles):包含一个能被启动的可执行文件,一个描述可执行文件的 Info.plist 文件,应用图标,启动图片,能被可执行文件调用的接口文件,字符串文件,以及数据文件。
  • 框架包(Framework Bundles):包含动态分享库所需要的代码和资源。
  • 可加载包(Loadable Bundles):类似于插件,包含扩展应用功能的可执行代码和资源。

访问包内容

对于应用,playgrounds,以及其它你感兴趣的包来说,都能通过 Bundle.main 进行访问。大多数情况,可以使用 url(forResource:withExtension:)(或它的一种变体)来获取特定资源的路径。

举例来说,如果应用中包含了一个名叫 Photo.jpg 的文件,用下面的方法能获得访问它的 URL:

Bundle.main.url(forResource: "Photo", withExtension: "jpg")

如果使用 Asset Catalog,你可以从媒体库(M)拖拽到编辑器来创建图像。

除此之外,Bundle 提供了一些实例方法和变量来获取标准包内容的位置,返回 URL 或 String 类型的路径:

URL Path 描述
executableURL executablePath 可执行文件
url(forAuxiliaryExecutable:) path(forAuxiliaryExecutable:) 辅助的可执行文件
resourceURL resourcePath 包含资源的子目录
sharedFrameworksURL sharedFrameworksPath 包含共享框架的子目录
privateFrameworksURL privateFrameworksPath 包含私有框架的子目录
builtInPlugInsURL builtInPlugInsPath 包含插件的子目录
sharedSupportURL sharedSupportPath 包含共享支援文件的子目录
appStoreReceiptURL App Store 的收据

获取应用信息

所有的应用包都必须有一个包含应用信息的 Info.plist 文件。

bundleURLbundleIdentifier 这样的原数据能够通过 bundle 实例被直接访问。

import Foundation

let bundle = Bundle.main

bundle.bundleURL // "/path/to/Example.app"
bundle.bundleIdentifier // "com.nshipster.example"

通过下标能从 infoDictionary 变量获得其他信息(如果信息要展示给用户,请使用 localizedInfoDictionary)。

bundle.infoDictionary["CFBundleName"] // "Example"
bundle.localizedInfoDictionary["CFBundleName"] // "Esempio" (`it_IT` locale)

获取本地化字符串

包的存在让本地化变得容易。强制本地化资源的存放位置后,系统便能将加载哪个版本的文件的逻辑从开发者层面抽象出来。

举个例子,包负责加载应用的本地化字符串。使用 localizedString(forKey:value:table:) 方法就可以获取到这些值。

import Foundation

let bundle = Bundle.main
bundle.localizedString(forKey: "Hello, %@",
value: "Hello, ${username}",
table: nil)

然而,通常来说用 NSLocalizedString 会更好,像 genstrings 这样的工具能够自动取出键和注释到 .strings 文件中便于翻译。

// Terminal
$ find . \( -name "*.swift" ! \ # 找出所有 swift 文件
! -path "./Carthage/*" \ # 无视 Carthage 与 CocoaPods 的依赖
! -path "./Pods/*"
\) | \
tr '\n' '\0' | \ # 替换分隔符
xargs -0 genstrings -o . \ # 处理带空格的路径

NSLocalizedString("Hello, %@", comment: "Hello, ${username}")

包裹(Packages)

包裹把相关资源封装和加固成一个独立单元,意在提升用户体验

满足以下任意一个条件,目录就会被访达认为是包裹:

  • 目录有类似于 .app.playground.plugin 等特殊扩展。
  • 目录有一个被一个应用注册作为文档类型的扩展。
  • 目录具有有扩展属性,将其指定为包裹。

访问包裹中的内容

在访达中,右键展示选中项目的可操作目录。如果选中项目是包裹,“打开”操作下会出现“显示包内容”选项。

点击这个选项会从包裹目录打开一个新的访达窗口。

当然,也可以通过代码访问包裹中的内容。包裹的类型决定了获取内容的最佳方式:

  • 如果包裹有包的结构,前文所说的 Bundle 就能轻松胜任。
  • 如果包裹是一个文档,在 macOS 上使用 NSDocument 或在 iOS 上使用 UIDocument 来访问。
  • 其他情况下,用 FileWrapper 导航目录,文件和符号链接,用 FileHandler 来读写文件描述。

判断一个目录是否是包裹

虽说是由访达决定如何展示文件和目录,大多数的判断会被代理给操作系统以及管理统一类型标识(UTI)的服务。

如果想要确定一个文件扩展是一个内置系统包裹类型,还是一个被已安装的应用使用的文档类型,调用 Core Services 方法 UTTypeCreatePreferredIdentifierForTag(_:_:_:)UTTypeConformsTo(_:_:) 能满足你的需求:

import Foundation
import CoreServices

func directoryIsPackage(_ url: URL) -> Bool {
let filenameExtension: CFString = url.pathExtension as NSString
guard let uti = UTTypeCreatePreferredIdentifierForTag(
kUTTagClassFilenameExtension,
filenameExtension, nil
)?.takeRetainedValue()
else {
return false
}

return UTTypeConformsTo(uti, kUTTypePackage)
}

let xcode = URL(fileURLWithPath: "/Applications/Xcode.app")
directoryIsPackage(xcode) // true

我们找不到任何描述如何设置所谓的包裹比特(package bit)的文档,但根据 CarbonCore/Finder.h,在 com.apple.FindlerInfo 扩展参数中设置 kHasBundle(0x2000) 标示能够实现:

> $ xattr -wx com.apple.FinderInfo /path/to/package \
> 00 00 00 00 00 00 00 00 20 00 00 00 00 00 00 00 \
> 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
>


正如我们看到的那样,并非只有终端用户从抽象中获益 —— 无论是像 Swift 这样的高级编程语言的安全性和表现力,还是像 Foundation 这样的 API 的便利性,作为开发者也可以利用抽象开发出优秀的软件。

或许我们会抱怨 抽象泄漏抽象反转 带来的问题,但重要的是退一步,了解我们每天处理多少有用的抽象,以及它们带给了我们多少可能性。

本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 http://swift.gg

❌