在 Swift 裡頭呼叫 CFArray

在寫 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 寫會像這樣:


	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) -> CGGradient!

看起來我們非把一個 CFArray 傳遞進去不可。那我們來看 Swift 裡頭 CFArrayCreate 又是怎麼宣告的:

func CFArrayCreate(allocator: CFAllocator!, values: UnsafeMutablePointer>, numValues: CFIndex, callBacks: UnsafePointer) -> CFArray!

呃…誰能夠告訴我 UnsafeMutablePointer> 是什麼鬼東西。然後根據蘋果文件,無論是 Objective-C 或是 Swfit 的版本,第二個參數 values 都需要傳入「A C array of the pointer-sized values to be in the new array」—在 Swift 裡頭又要怎麼建立 C Array 呢?

試了老半天,你最後發現,雖然 CGGradientCreateWithColors 在 colors 這個參數的地方,跟你要一個 CFArray,但是你直接把 Swift Array 傳進去就可以了,locations 要求,UnsafePointer,也可以直接傳入 Swift Array。colors 你就直接傳入 [CGColorRef],locations 就直接傳入 [Float],對應到 C API,一個要 CFArray,一個要 C Array,在 Swift 裡頭卻都可以傳入 Swift Array—你還真搞不懂 Swift 裡頭一個 Array 到底可以變成多少種不同的東西。

所以這段 code 可以寫成這樣:


	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:


	@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 這端要這麼寫:


	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 的指標,再轉成字串的話,像這樣:


	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 就得這麼寫。


	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
	}

Be Sociable, Share!

Leave a Reply