要不要来点 Swift

做者:Jacob Bandes-Storch,原文连接,原文日期:2015-11-28
译者:mmoaay;校对:千叶知风;定稿:shankshtml

作程序员有一点优点:若是工具很差用,你本身就能够对它进行优化。而 Swift 让这一点变得尤为简单,它包含的几个特性可让你以一种天然的方式对这门语言进行扩展和自定义。ios

在本文中,我将分享 Swift 给我编程体验带来提高的几个例子。我但愿在读了本文以后,你能够认识到使用这门语言时你本身的痛点在哪,并付诸实践。(固然须要先思考!)git

存在争议的重复标识符

下面是你在 Objective-C 中很熟悉的一种状况:枚举值和字符串常量会有很长的描述详细的名字:程序员

label.textAlignment = NSTextAlignmentCenter;

(这让我想起了中学科学课的格言:在做答时重复一下问题,或者 RQIA,文字是怎么对齐的?文字是居中对齐的。 这在做答方式在超出上下文环境的时候颇有用,可是其余状况下就显得比较冗余了。)github

Swift 减小了这种冗余,由于枚举值能够经过类型名+点符号来访问,并且若是你省略了类型名,它仍然能够被自动推断出来:编程

label.textAlignment = NSTextAlignment.Center

// 更简明的:
label.textAlignment = .Center

但有时候你用的不是枚举,而是被一个又臭又长的构造器给困住了。swift

animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)

有多少 “timingFunction” 呢?太多了好嘛。ruby

一个不那么为人所知的事实是,缩写点符号对任何类型的任何 static 成员都有效。结合在 extension 中添加自定义 property 的能力,咱们获得以下代码…闭包

extension CAMediaTimingFunction
{
    // 这个属性会在第一次被访问时初始化。
    // (须要添加 @nonobjc 来防止编译器
    //  给 static(或者 final)属性生成动态存取器。)
    @nonobjc static let EaseInEaseOut = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)

    // 另一个选择就是使用计算属性, 它一样颇有效,
    // 但 *每次* 被访问时都会从新求值:
    static var EaseInEaseOut: CAMediaTimingFunction {
        // .init 是 self.init 的简写
        return .init(name: kCAMediaTimingFunctionEaseInEaseOut)
    }
}

如今咱们获得了一个优雅的简写:app

animation.timingFunction = .EaseInEaseOut

Context 中的重复标识符

用来处理 Core Graphics Context、颜色空间等的代码每每也是冗长的。

CGContextSetFillColorWithColor(UIGraphicsGetCurrentContext(),
    CGColorCreate(CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB), [0.792, 0.792, 0.816, 1]))

再次使用棒棒的 extension

extension CGContext
{
    static func currentContext() -> CGContext? {
        return UIGraphicsGetCurrentContext()
    }
}

extension CGColorSpace
{
    static let GenericRGB = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB)
}

CGContextSetFillColorWithColor(.currentContext(),
    CGColorCreate(.GenericRGB, [0.792, 0.792, 0.816, 1]))

更简单了是不?并且显然会有更多的方式Core Graphics 类型进行扩展,以使其适应你的需求。

Auto Layout 中的重复标识符

下面的代码看起来熟悉么?

spaceConstraint = NSLayoutConstraint(
    item: label,
    attribute: .Leading,
    relatedBy: .Equal,
    toItem: button,
    attribute: .Trailing,
    multiplier: 1, constant: 20)
widthConstraint = NSLayoutConstraint(
    item: label,
    attribute: .Width,
    relatedBy: .LessThanOrEqual,
    toItem: nil,
    attribute: .NotAnAttribute,
    multiplier: 0, constant: 200)

spaceConstraint.active = true
widthConstraint.active = true

理解起来至关困难,是么?Apple 认识到这是个广泛存在的问题,因此从新设计了新的 NSLayoutAnchor API(在 iOS9 和 OS X 10.11 上适用)来处理这个问题:

spaceConstraint = label.leadingAnchor.constraintEqualToAnchor(button.trailingAnchor, constant: 20)
widthConstraint = label.widthAnchor.constraintLessThanOrEqualToConstant(200)
spaceConstraint.active = true
widthConstraint.active = true

然而,我认为还能够作的更好。在我看来,下面的代码比内置的接口更容易阅读和使用:

spaceConstraint = label.constrain(.Leading, .Equal, to: button, .Trailing, plus: 20)
widthConstraint = label.constrain(.Width, .LessThanOrEqual, to: 200)

// "设置 label 的 leading edge 和 button 的 trailing edge 相距 20"
// "设置 label 的 width 小于等于 200。"

上面的代码是经过给 UIView 或者 NSView 添加一些 extension 来实现的。这些辅助函数看起来可能会有些拙劣,可是用起来会特别方便,并且很容易维护。(这里我已经提供了另一些包含默认值的参数——一个 multiplierpriorityidentifier ——因此你能够选择更进一步滴进行自定义约束。)

extension UIView
{
    func constrain(
        attribute: NSLayoutAttribute,
        _ relation: NSLayoutRelation,
        to otherView: UIView,
        _ otherAttribute: NSLayoutAttribute,
        times multiplier: CGFloat = 1,
        plus constant: CGFloat = 0,
        atPriority priority: UILayoutPriority = UILayoutPriorityRequired,
        identifier: String? = nil)
        -> NSLayoutConstraint
    {
        let constraint = NSLayoutConstraint(
            item: self,
            attribute: attribute,
            relatedBy: relation,
            toItem: otherView,
            attribute: otherAttribute,
            multiplier: multiplier,
            constant: constant)
        constraint.priority = priority
        constraint.identifier = identifier
        constraint.active = true
        return constraint
    }
    
    func constrain(
        attribute: NSLayoutAttribute,
        _ relation: NSLayoutRelation,
        to constant: CGFloat,
        atPriority priority: UILayoutPriority = UILayoutPriorityRequired,
        identifier: String? = nil)
        -> NSLayoutConstraint
    {
        let constraint = NSLayoutConstraint(
            item: self,
            attribute: attribute,
            relatedBy: relation,
            toItem: nil,
            attribute: .NotAnAttribute,
            multiplier: 0,
            constant: constant)
        constraint.priority = priority
        constraint.identifier = identifier
        constraint.active = true
        return constraint
    }
}

你好~操做符

首先我必须提醒一下:若是要使用自定义操做符,必定要三思然后行。使用它们很简单,但最终可能会获得一堆像屎同样的代码。必定要对代码的健康性持怀疑态度,而后你会发如今某些场景下,自定义操做符确实是颇有用的。

重载它们

若是你有开发过拖动手势相关的功能,你可能会写过相似下面的代码:

// 触摸开始 / 鼠标按下:

let touchPos = touch.locationInView(container)
objectOffset = CGPoint(x: object.center.x - touchPos.x, y: object.center.y - touchPos.y)

// 触摸移动 / 鼠标拖动:

let touchPos = touch.locationInView(container)
object.center = CGPoint(x: touchPos.x + objectOffset.x, y: touchPos.y + objectOffset.y)

在这段代码里面咱们只作了简单的加法和减法,但由于 CGPoint 包含 xy,因此每一个表达式咱们都要写两次。因此咱们须要一些简化操做的函数。

objectOffset 表明触摸位置和对象位置的距离。描述这种距离最好的方式并非 CGPoint,而是不那么为人所知的 CGVector, 它不使用 xy,而是用 dxdy 来表示距离或者 “deltas“。

这里写图片描述

因此两个点相减获得一个向量就比较符合逻辑了,这样一来咱们就获得了 - 操做符的一个重载:

/// - 返回: 从 `rhs` 到 `lhs`的向量。
func -(lhs: CGPoint, rhs: CGPoint) -> CGVector
{
    return CGVector(dx: lhs.x - rhs.x, dy: lhs.y - rhs.y)
}

而后,相反滴,把一个向量和一个点相加获得另一个点:

// - 返回: `lhs` 偏移`rhs` 以后获得的一个点
func +(lhs: CGPoint, rhs: CGVector) -> CGPoint
{
    return CGPoint(x: lhs.x + rhs.dx, y: lhs.y + rhs.dy)
}

如今下面的代码看起来就感受很好了!

// 触摸开始:
objectOffset = object.center - touch.locationInView(container)

// 触摸移动:
object.center = touch.locationInView(container) + objectOffset

练习:想想其它能够用在点和向量上的操做符,并对它们进行重载。
建议:-(CGPoint, CGVector)*(CGVector, CGFloat)-(CGVector)

独门绝技

下面是一些更有创造性的内容。Swift 提供了一些复合赋值操做符,这些操做符在执行某个操做的同时进行赋值:

a += b   // 等价于 "a = a + b"
a %= b   // 等价于 "a = a % b"

可是仍然存在其它不包含内置复合赋值形式的操做符。最多见的例子就是 ??,空合并运算符。也就是 Ruby 中的 ||=,例如,首先实现只有在变量是 nil (或者不是)的状况下才赋值的版本。这对 Swift 中的可选值意义非凡,并且实现起来也很简单:

infix operator ??= { associativity right precedence 90 assignment } // 匹配其它的赋值操做符

/// 若是 `lhs` 为 `nil`, 把 `rhs` 的值赋给它
func ??=<T>(inout lhs: T?, @autoclosure rhs: () -> T)
{
    lhs = lhs ?? rhs()
}

这段代码看起来可能很复杂——这里咱们作了下面几件事情。

  • infix operator 声明用来告诉 Swift 把 ??= 看成一个操做符。

  • 使用 <T> 将函数泛型化,从而使其能够支持任何类型的值。

  • inout 表示容许修改左侧的运算数

  • @autoclosure 用来支持短路赋值,有须要的话能够只对右侧作赋值操做。(着也依赖于 ?? 自己对短路的支持。)

但在我看来,上述代码实现的功能是很是清晰并且易用的:

a ??= b   // 等价于 "a = a ?? b"

调度

关于如何在 Swift 中使用 GCD,最好的方式是阅读官方文档,但在本文中我仍然会介绍一些基础知识点。若是想了解更多,请参照这份概要

Swift 2 引入了协议扩展,所以,不少以前的全局标准库函数变成了准成员函数:如 map(seq, transform) 变成了如今的 seq.map(transform)join(separator, seq) 变成了如今的 seq.joinWithSeparator(separator) 等等。这样的话,那些严格说来不属于类实例方法的函数仍然能够用 . 符号访问,并且还减小了逗号(PS:原文为parentheses,多是做者笔误)的数目,从而不会把代码弄得太乱

然而这种变化并无应用到 Swift 标准库外的自由函数,好比 dispatch_async()UIImageJPEGRepresentation()。这些函数仍然很难用,若是你常用它们,仍是很值得思考一下如何利用 Swift 来帮你改造一下它们。下面是一些入门的 GCD 例子。

sync 或者非 sync

这些都很简单;咱们立刻开始:

extension dispatch_queue_t
{
    final func async(block: dispatch_block_t) {
        dispatch_async(self, block)
    }
    
    // 这里的 `block` 须要是 @noescape 的, 但不能是连接中这样的: <http://openradar.me/19770770>
    final func sync(block: dispatch_block_t) {
        dispatch_sync(self, block)
    }
}

上面简化的两个函数直接调用了普通的调度函数,但可让咱们经过 . 符号调用它们,这是咱们以前作不到的。

注:GCD 对象是以一种古怪的方式导出到 Swift 的,尽管以类的方式也能够实现,但实际上 dispatch_queue_t 只是一个协议而已。在这里我把两个函数都标注了 final 来代表咱们的意图:咱们不但愿在这里使用动态调度,尽管在我看来这种状况下使用协议扩展是很好的,可是不要在哪都用。

mySerialQueue.sync {
    print("I’m on the queue!")
    threadsafeNum++
}

dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0).async {
    expensivelyReticulateSplines()
    print("Done!")
    
    dispatch_get_main_queue().async {
        print("Back on the main queue.")
    }
}

更进一步的 sync 的版本是从 Swift 标准库函数中的 with* 家族获取到的灵感,咱们要作的事情是返回一个在闭包中计算获得的结果:

extension dispatch_queue_t
{
    final func sync<Result>(block: () -> Result) -> Result {
        var result: Result?
        dispatch_sync(self) {
            result = block()
        }
        return result!
    }
}

// 在串行队列上抓取一些数据
let currentItems = mySerialQueue.sync {
    print("I’m on the queue!")
    return mutableItems.copy()
}

群体思惟

另外两个简单的扩展可让咱们很好的使用 dispatch group

extension dispatch_queue_t
{
    final func async(group: dispatch_group_t, _ block: dispatch_block_t) {
        dispatch_group_async(group, self, block)
    }
}

extension dispatch_group_t
{
    final func waitForever() {
        dispatch_group_wait(self, DISPATCH_TIME_FOREVER)
    }
}

如今调用 async 的时候就能够包含一个额外的 group 参数了。

let group = dispatch_group_create()

concurrentQueue.async(group) {
    print("I’m part of the group")
}

concurrentQueue.async(group) {
    print("I’m independent, but part of the same group")
}

group.waitForever()
print("Everything in the group has now executed")

注:咱们能够很简单滴选择 group.async(queue) 或者 queue.async(group)。具体用哪一个全看你本身——或者你甚至能够两个都实现。

优雅的重定义

若是你的项目同时包含 Objective-C 和 Swift,你可能会碰到这种让人头大的状况:Obj-C 的 API 看起来不是那么 Swift 化。须要 NS_REFINED_FOR_SWIFT 来拯救咱们了。

在 Obj-C 中使用标记了 (new in Xcode 7) 宏的函数、方法和变量是正常的,可是导出到 Swift 以后,它们会包含一个 “__“前缀。

@interface MyClass : NSObject

/// @返回 @c 东西的下标, 若是没有提供就返回 NSNotFound。
- (NSUInteger)indexOfThing:(id)thing NS_REFINED_FOR_SWIFT;

@end

// 当导出到 Swift 时, 它就变成了:

public class MyClass: NSObject
{
    public func __indexOfThing(thing: AnyObject) -> UInt
}

如今把 Obj-C 的方法放到一边,你能够重用一样的名字来提供一个更友好的 Swift 版本 API(实现方式一般是调用带“__“前缀的原始版本):

extension MyClass
{
    /// - 返回: 给定 `thing` 的下标, 若是没有就返回 `nil`。
    func indexOfThing(thing: AnyObject) -> Int?
    {
        let idx = Int(__indexOfThing(thing)) // 调用原始方法
        if idx == NSNotFound { return nil }
        return idx
    }
}

如今你能够心满意足滴 “if let“了!

更进一步

Swift 还很年轻,它各个代码库的风格也都是不一样的。而大量的第三方微型库也涌现出来,这些库的代码体现了做者在操做符,辅助函数和命名规范上所持的不一样观点。这种状况也就须要在处理依赖关系以及在团队中创建规范时更挑剔。

使用本文中技术最重要的缘由不是写最新潮和最酷炫的 Swift 代码。固然,负责维护你代码的人——也许是将来的你——可能会持不一样的观点。为了他们,亲爱的读者,你须要在为了让代码变的清晰合理的状况下扩展 Swift,而不是为了让代码变的简单而去扩展它。

译者:Playground已经上传到github!

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

相关文章
相关标签/搜索