當UIColor遇上Swift

bestswifter發表於2018-01-03

我為這篇文章製作了一個demo,已經上傳到我的GitHub:KTColor,如果覺得有幫助還望給個star以示支援。

UIColor 提供了幾個預設的顏色,要想建立除此以外的顏色,一般是通過RGB和alpha值建立(十六進位制的顏色其實也是被轉換成RGB)。在 Objective-C 中,這可以通過自定義巨集來完成,在 Swift 中,我們可以利用 Swift 的一些語法特性來簡化建立 UIColor 物件的過程。我想,最理想的解決方案應該是這樣:

override func viewDidLoad() {
super.viewDidLoad()

self.view.backgroundColor = "224, 222, 255"
}
複製程式碼

變通方案

然而很不幸的是,在目前的 Swift 版本(2.1)中,這種寫法暫時無法實現。據我所知,Swift3.0 也不支援這種寫法,原因會在稍後分析。目前,我們可以使用兩種變通方案:

self.view.backgroundColor = "224, 222, 255".ktColor    // 方案1
self.view.backgroundColor = "224, 222, 255" as KtColor    // 方案2
複製程式碼

兩者寫法類似,但實現原理實際上完全不同。第一種方案是通過擴充 String 型別實現的,第二種方案則是通過繼承 UIColor 實現。

方案1有更好的程式碼提示,但它對 String 型別作了修改,我的demo中有完整的實現,它支援以下輸入:

self.view.backgroundColor = "224, 222, 255, 0.5".ktcolor // 這個是完整版
self.view.backgroundColor = "224, 222, 255".ktcolor // alpha值預設為1
self.view.backgroundColor = "224,222,255".ktcolor // 可以用逗號分割
self.view.backgroundColor = "224 222 255".ktcolor // 可以用空格分割
self.view.backgroundColor = "#DC143C".ktcolor  // 可以使用16進位制數字
self.view.backgroundColor = "#dc143c".ktcolor  // 字母可以小寫
self.view.backgroundColor = "SkyBlue".ktcolor  // 可以直接使用顏色的英文名字
複製程式碼

雖然方案2不會對現有程式碼做修改,但它並不適用於所有系統型別,比如 NSDateNSURL 型別,出於這種考慮,demo中僅實現了關鍵邏輯。但這種實現方法最接近於理想的解決方案,一旦時機合適,我們就可以去掉醜陋的 as KtColor

擴充字串

第一種方案通過擴充 String 型別實現,它新增了一個 ktcolor 計算屬性,主要涉及到字串的分割與處理,還有一些容錯、判斷等,這些就不是本文的重點了,如果有興趣,讀者可以通過閱讀原始碼獲得更加深入的瞭解。

這種方案的好處在於它還適用於 NSDateNSURL等型別。比如,下面的程式碼可以通過類似的技術實現:

let date = "2016-02-17 24:00:00".ktdate
let url = "http://bestswifter.com".kturl
複製程式碼

不過,方案一選擇的技術註定了它沒有再簡化的空間了。如果不能顯著的減少程式碼量,它就沒有理由取代原生的方案。

字串字面量

方案二和理想方案採用的都是同一個思路:“利用字串字面量建立物件”。在我的這篇文章中對此有比較詳細的解釋。

簡單來說,我們要做的只是為 UIColor 型別新增如下的擴充:

extension UIColor: StringLiteralConvertible {
public init(stringLiteral value: String) {
//這裡的數字是隨便寫的,實際上需要解析字串
self.init(red: 0.5, green: 0.8, blue: 0.25, alpha: 1)
}

public init(extendedGraphemeClusterLiteral value: String) {
self.init(stringLiteral: value)
}

public init(unicodeScalarLiteral value: String) {
self.init(stringLiteral: value)
}
}
複製程式碼

不過你會收到這樣的報錯:

Initializer requirement 'init(stringLiteral:)' can only be satisfied by a required initializer in the definition of non-final class 'UIColor'

Xcode 的報錯有時候不愛說人話,其實這句話的意思是說,'UIColor' 不是一個標記為 final 的類,也就是它還可以被繼承。因此 init(stringLiteral:) 函式需要被標記為 required 以確保所有子類都實現了這個函式。否則,如果有子類沒有實現它,那麼子類就不滿足 StringLiteralConvertible 協議。

好吧,我們聽從 Xcode 的指示,把每個函式都標記為 required,新的問題又出現了:

'required' initializer must be declared directly in class 'UIColor' (not in an extension)

這是因為 Swift 不允許在型別擴充中宣告 required 函式。required 函式必須被直接宣告在類的內部。

這就導致了一個死迴圈,因此目前理想方案無法實現,除非未來 Swift 允許在擴充中宣告required 函式。

繼承

方案二採用了變通的解決方案。首先建立一個 UIColor 的子類,我們可以讓這個子類實現 StringLiteralConvertible 協議,然後將子類物件賦值給父類:

class KtColor: UIColor, StringLiteralConvertible {
required init(stringLiteral value: String) {
//這裡的數字是隨便寫的,實際上需要解析字串
super.init(red: 0.5, green: 0.8, blue: 0.25, alpha: 1)
}

required convenience init(extendedGraphemeClusterLiteral value: String) {
self.init(stringLiteral: value)
}

required convenience init(unicodeScalarLiteral value: String) {
self.init(stringLiteral: value)
}

required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

required convenience init(colorLiteralRed red: Float, green: Float, blue: Float, alpha: Float) {
self.init(colorLiteralRed: red, green: green, blue: blue, alpha: alpha)
}
}

override func viewDidLoad() {
super.viewDidLoad()

self.view.backgroundColor = "224, 222, 255" as KtColor
}
複製程式碼

這種方法有一個顯而易見的好處,一旦 Swift 做出了修改,比如允許在擴充中宣告required 函式,我們只需要微小的改動就可以實現理想方案。

侷限性

繼承 UIKit 中的類並不總是一種可行的方法。比如 NSDate 類其實是一個類簇,官網對它有如下解釋:

The major reason for subclassing NSDate is to create a class with convenience methods for working with a particular calendrical system. But you could also require a custom NSDate class for other reasons, such as to get a date and time value that provides a finer temporal granularity. If you want to subclass NSDate to obtain behavior different than that provided by the private or public subclasses, you must do these things:

列出了一大串你根本不想去做的事,省略一萬字。。。。。。

簡單來說,如果你想繼承 NSDate,就必須重新實現它。

除了類簇,像 NSURL 這樣,指定建構函式是可失敗建構函式的類也無法使用繼承:

class KtURL : NSURL, StringLiteralConvertible {
required init(stringLiteral value: StringLiteralType) {
super.init(string: value, relativeToURL: nil)
}
// 其他的函式略
}
複製程式碼

StringLiteralConvertible協議中定義的建構函式是不可失敗建構函式,它不會返回 nil。在它的內部呼叫了父類,也就是 NSURLinit(string:relativetoURL:),這是可失敗建構函式。Swift不允許出現這種情況,否則如果傳入的引數 value 不合法,你會得到 nil麼,如果不是 nil 那麼會得到什麼?

總結

完全使用字串字面量建立已有類的例項變數在目前是無法實現的,一種想法類似但是存在侷限性的方法是使用子類。或者也可以擴充 String型別,但如果相比於原生實現,不能較大幅度的減少程式碼量,我不建議這麼做。

參考資料:

  1. devforums.apple.com/message/105…:這個好像是喵神提的問題。

  2. Swift: Simple, Safe, Inflexible

相關文章