在寫 Swift 的時候遇到 CFArray 都得要花點時間想一下。雖然當你在使用 Swift 語言的時候,你會盡可能希望只呼叫到 Swift 與 Objective-C API,而不用碰到 Core Foundation 的 C API,可是呢,當你在做某些事情的時候,就還是得遇到這些東西。
在 Swift 裡頭處理 CFArray 之所以不好搞,第一個原因是,當某個 API 希望你傳入 CFArray 的時候,建立 CFArray 的方式跟你在 C/Objective-C code 裡頭差異很大,但似乎也沒有看到多少文件在說明這點。
另外當你從 C/Objective-C code 傳遞了一個 CFArray 到 Swift 裡頭的時候,裡頭可能會有兩種類型的東西:一種是可以 bridge 成 Objective-C 或是 Swift 的物件—通常是 Foundation 或是 Core Foundation 的物件;另外也可能是其他種類的 C 指標,可能是某種另外定義的 C Structure,而在這兩種不同的狀態下,要用不同的方式操作 CFArray。
在 Swift 裡頭建立 CFArray
先舉個例子。如果我們想要在某個 UIView 裡頭畫一個漸層,一個方法是在這個 view 的 layer 上面再多加一個 CAGradientLayer,另一種方法則是 override 掉 drawRect:。用 C/Objective-C 寫會像這樣:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 | CGContextRef ctx = UIGraphicsGetCurrentContext(); CGColorRef beginColor = [UIColor whiteColor].CGColor; CGColorRef endColor = [UIColor blackColor].CGColor; CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); CGColorRef colorsCArray[2] = {beginColor, endColor}; CFArrayRef colors = CFArrayCreate( NULL , ( void *)colorsCArray, 2, NULL ); CGFloat locations[2] = {0.0, 1.0}; CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, colors, locations); CGPoint beginPoint = CGPointMake(0, 0); CGPoint endPoint = CGPointMake(0, CGRectGetMaxY( self .bounds)); CGContextDrawLinearGradient(ctx, gradient, beginPoint, endPoint, kCGGradientDrawsAfterEndLocation); CGColorSpaceRelease(colorSpace); CFRelease(colors); CGGradientRelease(gradient); |
我們該怎樣在 Swift 裡頭寫上面這段程式?CGGradientCreateWithColors 在 Swift 裡頭的宣告寫成:
func CGGradientCreateWithColors(space: CGColorSpace!, colors: CFArray!, locations: UnsafePointer
看起來我們非把一個 CFArray 傳遞進去不可。那我們來看 Swift 裡頭 CFArrayCreate 又是怎麼宣告的:
func CFArrayCreate(allocator: CFAllocator!, values: UnsafeMutablePointer
呃…誰能夠告訴我 UnsafeMutablePointer
試了老半天,你最後發現,雖然 CGGradientCreateWithColors 在 colors 這個參數的地方,跟你要一個 CFArray,但是你直接把 Swift Array 傳進去就可以了,locations 要求,UnsafePointer
所以這段 code 可以寫成這樣:
1 2 3 4 5 6 | let ctx = UIGraphicsGetCurrentContext() let beginColor = UIColor.whiteColor().CGColor let endColor = UIColor.blackColor().CGColor let colorSpace = CGColorSpaceCreateDeviceRGB() let gradient = CGGradientCreateWithColors(colorSpace, [beginColor, endColor], [0.0, 1.0]) CGContextDrawLinearGradient(ctx, gradient, CGPointMake(0, 0), CGPointMake(0, CGRectGetMaxY( self .bounds)), CGGradientDrawingOptions(kCGGradientDrawsAfterEndLocation)) |
在 Swift 裡頭讀取從 C/Objective-C 產生的 CFArray
假如我們今天在 C/Objective-C code,寫了一個包含 Core Foundation 物件的 CFArray,想要傳給 Swift code:
01 02 03 04 05 06 07 08 09 10 11 | @implementation ObjCArrayMaker - (CFArrayRef)strings { CFStringRef str1 = CFSTR( "Hi 1" ); CFStringRef str2 = CFSTR( "Hi 2" ); CFStringRef strArray[2] = {str1, str2}; CFArrayRef strs = CFArrayCreate( NULL , ( void *)strArray, 2, NULL ); CFAutorelease(strs); return strs; } @end |
我們在 Swift 這端要這麼寫:
1 2 3 4 | var maker = ObjCArrayMaker() var strs = maker.strings().takeUnretainedValue() as [String] println( "\(strs[0])" ) println( "\(strs[1])" ) |
輸出結果會是「Hi 1」與「Hi 2」。
我們可以把這個 CFArray 當做 Swift Array 操作。但,假如我們用 CFArrayGetValueAtIndex 操作這邊傳入的 CFArray,想嘗試拿出 CFArray 中每個 index 的指標,再轉成字串的話,像這樣:
1 2 3 4 5 6 7 8 | var maker = ObjCArrayMaker() var strs = maker.strings().takeUnretainedValue() var strPtr1 = CFArrayGetValueAtIndex(strs, 0) var strPtr2 = CFArrayGetValueAtIndex(strs, 1) var str1 :CFString = UnsafePointer<CFString>(strPtr1).memory as CFString var str2 :CFString = UnsafePointer<CFString>(strPtr2).memory as CFString println( "\(str1)" ) println( "\(str2)" ) |
在 Console 上,會印出「__NSCFConstantString」這樣的東西,如果你繼續想用 str1 與 str2 做些事情,就很有可能因為記憶體管理問題而直接 crash。
但,就像前面說的,CFArray 裡頭除了可能出現 Foundation/Core Foudation 之類可以 bridge 的物件,也可能出現其他種類的 C Structure。
比方說,你可能在 Objective-C code 這邊,用 Core Audio 寫了一個 audio player,這個 audio player 透過 EQ effect node 實作了 EQ 等化器功能,你現在打算用 Swift code,寫一個 table view,把所有的等化器設定顯示出來。取得等化器設定的方式,是對 kAudioUnitSubType_AUiPodEQ 類型的 node,要求取得 kAudioUnitProperty_FactoryPresets,你會拿到一個由 AUPresets 組成的 CFArray。你的 table view 就得這麼寫。
1 2 3 4 5 6 7 8 | override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath ) -> UITableViewCell { var cell :UITableViewCell = tableView.dequeueReusableCellWithIdentifier( "Cell" , forIndexPath: indexPath) as UITableViewCell let presets :CFArrayRef = myPlayer.iPodEQPresetsArray let presetPtr = CFArrayGetValueAtIndex(presets, indexPath.row) let preset = UnsafePointer<AUPreset>(presetPtr).memory as AUPreset cell.textLabel?.text = preset.presetName.takeUnretainedValue() as String return cell } |