做者: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
類型進行擴展,以使其適應你的需求。
下面的代碼看起來熟悉麼?
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
來實現的。這些輔助函數看起來可能會有些拙劣,可是用起來會特別方便,並且很容易維護。(這裏我已經提供了另一些包含默認值的參數——一個 multiplier
,priority
和 identifier
——因此你能夠選擇更進一步滴進行自定義約束。)
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
包含 x
和 y
,因此每一個表達式咱們都要寫兩次。因此咱們須要一些簡化操做的函數。
objectOffset
表明觸摸位置和對象位置的距離。描述這種距離最好的方式並非 CGPoint
,而是不那麼爲人所知的 CGVector, 它不使用 x
和 y
,而是用 dx
和 dy
來表示距離或者 「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,而不是爲了讓代碼變的簡單而去擴展它。
本文由 SwiftGG 翻譯組翻譯,已經得到做者翻譯受權,最新文章請訪問 http://swift.gg。