作為一名軟體工程師,好處之一就是如果我們對手上的工具不甚滿意的話,我們可以自行對這個工具進行完善。Swift 讓這個優化過程變得更為輕鬆,它提供了許多特性從而允許我們能夠自然而然地擴充套件和自定義這門語言。
在本篇文章中,我打算為大家分享一系列 Swift 小貼士,以表述 Swift 是如何讓我的生活更加美好的。我希望本文能夠拋磚引玉,讓您對這門語言有更深甚至更好的想法,並付諸行動(切記要三思而後行!)。
幹掉重複的識別符號
您可能已經熟悉了 Objective-C 中的一個使用慣例:那就是列舉值以及其字元常量通常都有長得嚇人的描述名:
1 |
label.textAlignment = NSTextAlignmentCenter; |
(這讓我想起了從中學課上學習到的一條準則:在答案中要複述問題,簡稱 RQIA。問:在這個例子中是哪一個文字對齊方式呢?答:在這個例子中是居中文字對齊方式。這對老師批閱試卷來說是非常有效的一種做法,因為老師很可能記不住問題,但是在其他情況下這種做法會顯得十分繁雜。)
Swift 減少了冗餘度,因為列舉值可以在型別名之後加上點語法進行訪問,即使你省略掉了型別姓名它仍然能夠推斷出來:
1 2 3 4 |
label.textAlignment = NSTextAlignment.Center // 更為簡潔: label.textAlignment = .Center |
然而,很多時候我們很可能不會用到列舉,遇上的往往是這樣很長很長的一個構造器:
1 |
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) |
程式碼中會有多少個“timingFunction”?很可能多到無法想象。
有一個不為人知的小技巧,就是縮略形式的點語法對於所有型別的靜態成員來說都是有效的。通過在擴充套件中增加自定義的屬性就可以應用上這個技巧了……
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
extension CAMediaTimingFunction { // 這個屬性是懶載入屬性,第一次被訪問時才會被初始化。 // (@nonobjc 標記是必須的,這可以阻止編譯器試圖為一個 // 靜態屬性建立動態訪問器(也就是令其不可繼承)。 @nonobjc static let EaseInEaseOut = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) // 另一個方法就是使用計算性屬性,這也同樣有效, // 但是*每次*訪問它的時候都將重新計算,可能帶來效能問題: static var EaseInEaseOut: CAMediaTimingFunction { // .init is short for self.init return .init(name: kCAMediaTimingFunctionEaseInEaseOut) } } |
這樣我們就可以很方便地簡化這個操作了:
1 |
animation.timingFunction = .EaseInEaseOut |
上下文環境
處理 Core Graphics 上下文、色區之類的程式碼同樣也會非常非常長:
1 2 |
CGContextSetFillColorWithColor(UIGraphicsGetCurrentContext(), CGColorCreate(CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB), [0.792, 0.792, 0.816, 1])) |
我們仍然使用萬能的擴充套件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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,從而讓它符合您的需求。
自動佈局
這是不是似曾相識?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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 |
非常難以閱讀,是吧?蘋果意識到這是一個很常見的問題,因此重新設計了新的 NSLayoutAnchor API(可以在 iOS 9 以及 OS X 10.11 上使用)來簡化自動佈局的構造:
1 2 3 4 |
spaceConstraint = label.leadingAnchor.constraintEqualToAnchor(button.trailingAnchor, constant: 20) widthConstraint = label.widthAnchor.constraintLessThanOrEqualToConstant(200) spaceConstraint.active = true widthConstraint.active = true |
不過,我覺得我們可以做得更好。在我的設想當中,下面這行程式碼比內建的 API 更容易閱讀和使用:
1 2 3 4 5 |
spaceConstraint = label.constrain(.Leading, .Equal, to: button, .Trailing, plus: 20) widthConstraint = label.constrain(.Width, .LessThanOrEqual, to: 200) // "讓標籤的左邊緣和按鈕的右邊緣建立關聯,距離為 20" // "讓標籤的寬度小於或等於 200" |
我們可以對 UIView 或者 NSView 建立一對擴充套件,從而讓這個設想成為可能。這些輔助方法可能看起來又臭又長,但是它們使用起來卻十分方便,並且在可維護性上也是十分優越的。(這裡我實際上包含了一些帶有預設值的額外引數——比如 multiplier
、priority
以及 identifier
,因此你可以更好地對約束進行構建)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
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 } } |
你好啊,運算子
在我們使用自定義運算子之前,我必須要告誡大家:切記要三思而後行。運算子使用起來很簡單,但是很可能最後會搞得一團糟。一步一個腳印,對自己程式碼不要過分自信,我確信您最終就可以找到自定義運算子的真正用處。
過載運算子
如果您要讓一個元素可以拖動的話,那麼很可能就要寫如下所示的程式碼:
1 2 3 4 5 6 7 8 9 |
// 開始觸控 / 滑鼠摁下: 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 由 x
和 y
兩個元素組成,因此我們必須要再次寫下每個表示式。這裡使用了某些便利函式來實現。
objectOffset
代表了觸控位置和物件位置之間的距離。表述這個距離的最好方式實際上並不是 CGPoint,而是使用不常見的 CGVector,它使用的是 dx
和 dy
來表示距離,也就是所謂的“變數增量”。
因此,兩點之間的減法操作會生成一個向量是符合邏輯的,為了實現這個功能,我們需要過載 -
運算子:
1 2 3 4 5 |
/// - Returns: 由 `rhs` 指向 `lhs` 的一個向量 func -(lhs: CGPoint, rhs: CGPoint) -> CGVector { return CGVector(dx: lhs.x - rhs.x, dy: lhs.y - rhs.y) } |
然後反過來,我們向一個點上加上向量將會生成另一個點:
1 2 3 4 5 |
/// - Returns: 距離 `lhs` 點 `rhs` 矢距的新點 func +(lhs: CGPoint, rhs: CGVector) -> CGPoint { return CGPoint(x: lhs.x + rhs.dx, y: lhs.y + rhs.dy) } |
現在那堆程式碼就變得更加易懂了!
1 2 3 4 5 |
// 觸控開始: objectOffset = object.center - touch.locationInView(container) // 手指拖動: object.center = touch.locationInView(container) + objectOffset |
練習:想想對於點和向量來說還有哪些運算子可以用?找出並實現它們。建議:
-(CGPoint, CGVector)
、*(CGVector, CGFloat)
以及-(CGVector)
。
發揮創造力
還有一些東西更有創造性。Swift 提供了一系列 複合賦值運算子(compound assignment operators),同時執行算術操作以及賦值:
1 2 |
a += b // 等同於 "a = a + b" a %= b // 等同於 "a = a % b" |
但是仍然有很多運算子沒有內建的複合賦值形式。一個最常見的例子就是 ??
,空合(nil-coalescing)運算子。就如同 Ruby 的 ||=
只有當變數為空(或者為 false )的時候才開始賦值。這對於 Swift 可選值來說是一個非常好的特性,並且新增起來十分簡單:
1 2 3 4 5 6 7 |
infix operator ??= { associativity right precedence 90 assignment } // 匹配其他的賦值運算子 /// 如果 `lhs` 為 `nil`, 那麼就用 `rhs` 的值對其進行賦值 func ??=(inout lhs: T?, @autoclosure rhs: () -> T) { lhs = lhs ?? rhs() } |
這可能讓人望而卻步——我們一點一點來進行分析:
infix operator
宣告告訴 Swift 將??=
視為一個運算子。- 通過
為函式新增泛型,這樣可以處理所有型別的值。
inout
允許它修改左運算元。@autoclosure
將開啟短路求值(short-circuit evaluation)功能,如果可以的話只對右運算元進行求值(這個同樣也依賴於??
本身的短路求值功能)。
最後,結果如我所願,簡潔明瞭,易於使用:
1 |
a ??= b // 相當於 "a = a ?? b" |
排程佇列
注意:關於如何讓 Swift 恰當地使用 GCD(Grand Central Dispatch) 可能需要為其專門寫一篇文章,但是這裡我會盡可能將基礎部分說完。你可以在這個 Gist 上找到更多的想法。
Swift 2 引入了協議擴充套件,因此之前許多全域性標準庫函式變成了準成員函式,比如說 map(seq, transform)
現在變成了 seq.map(transform)
,join(separator, seq)
現在變成了 seq.joinWithSeparator(separator)
等等。因此,這些理論上不是例項方法的函式仍然可以使用點語法來進行訪問,減少一堆括號導致程式碼散亂無章的現象發生。
然而,所有的付出並沒有完全收穫,對於 Swift 標準庫之外的自由函式,比如說 dispatch_async()
以及 UIImageJPEGRepresentation()
。這些函式用起來十分笨重,如果您的程式碼中大量應用了這些函式的話,不妨來看看 Swift 是如何減輕您的負擔的。我們以一些 GCD 的例子開始。
sync
與 async
這十分簡單,我們直接上程式碼:
1 2 3 4 5 6 7 8 9 10 |
extension dispatch_queue_t { final func async(block: dispatch_block_t) { dispatch_async(self, block) } // `block` 這裡應該被標記為 @noescape,然而我們無法這麼做 final func sync(block: dispatch_block_t) { dispatch_sync(self, block) } } |
這兩個便利呼叫方法可以直接放入正常的佇列處理函式,並且允許我們使用點語法進行訪問,在此之前我們是沒辦法這麼做的。
注意:由於 GCD 物件轉換至 Swift 的方法十分古怪,
dispatch_queue_t
實際上是一個協議,雖然它也可以作為類正常工作。我這裡用final
給函式做了標記以表明我們的意圖,也就是不能在動態佇列中使用。雖然我的理解是這裡本質上是協議擴充套件,在與此類似的情況下,千萬不要使用它。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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*
族獲取到了靈感,讓我們在閉包中返回所計算出的值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
extension dispatch_queue_t { final func sync(block: () -> Result) -> Result { var result: Result? dispatch_sync(self) { result = block() } return result! } } // Grab some data while on the serial queue let currentItems = mySerialQueue.sync { print("I’m on the queue!") return mutableItems.copy() } |
群策群力
還有兩個簡單的擴充套件,通過 dispatch groups可以輕鬆地實現:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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) } } |
現在這個額外的 group
引數就可以被包含進 async
的常規呼叫當中了。
1 2 3 4 5 6 7 8 9 10 11 12 |
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
來救場了。
通過這個巨集指令(Xcode 7 新出的)所標記的函式、方法和變數在 Obj-C 程式碼中可以正常使用,但當它們橋接到 Swift 的時候,名稱前面就會加上“__”。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@interface MyClass : NSObject /// @return 指定 <a href='http://www.jobbole.com/members/522c'>@c</a> 的索引,如果未找到的話則返回 NSNotFound - (NSUInteger)indexOfThing:(id)thing NS_REFINED_FOR_SWIFT; @end // 當橋接到 Swift 的時候,這個方法變為: public class MyClass: NSObject { public func __indexOfThing(thing: AnyObject) -> UInt } |
通過 Objc-C 方法,您就可以使用相同的名稱來提供一個對 Swift 更友好的 API(通常在現有加下劃線的原始版本的基礎上來實現)。
1 2 3 4 5 6 7 8 9 10 |
extension MyClass { /// - Returns: 指定 `thing` 的索引,如果未找到的話返回 `nil` func indexOfThing(thing: AnyObject) -> Int? { let idx = Int(__indexOfThing(thing)) // 呼叫原始方法 if idx == NSNotFound { return nil } return idx } } |
現在我們就可以使用 if let
來使用這段程式碼了!
展望
Swift 是一門年輕的語言,其中每個程式碼庫(codebase)都是不同的。大量微型函式庫正在湧現,每位作者對於運算子、輔助方法以及命名規範都有著各自的標準。這個情況就需要蘋果團隊中需要謹慎採納依賴庫和標準庫設定。
採用本文所述的技術不是為了寫最酷、最時髦的 Swift 程式碼。誠然,維護您任務的某人——也是將來的您自己——可能會以一份全新的方式來思考這些程式碼。他們的目的只是為了更好地閱讀程式碼,因此不要因為怎樣簡單怎樣來,而是怎樣明瞭怎樣來。