How to convert CFArray to Swift Array?

2020-05-22 00:56发布

问题:

According to Apple's "Using Swift with Cocoa and Objective-C", "In Swift, you can use each pair of toll-free bridged Foundation and Core Foundation types interchangeably". This makes working with Core Foundation sound way simpler than it actually is...

I am trying to work with a CFArray that is returned from CoreText. I have this code:

let lines: CFArrayRef  = CTFrameGetLines(frame)

I see two possible ways to access members of this array. Neither is working for me right now.


Way #1 - Use the CFArray directly

let line: CTLineRef = CFArrayGetValueAtIndex(lines, 0)

This yields the error "'ConstUnsafePointer<()>' in not convertible to 'CTLineRef'". Casting does not seem to change this error.

Similarly, I would love to use lines "interchangeably" as a Swift array like it says that I can. However,

let line: CTLineRef = lines[0]

yields the error "'CFArrayRef' does not have a member named 'subscript'"


Way #2 - Convert the CFArray to a Swift array

var linesArray: Array = [CTLineRef]()
linesArray = bridgeFromObjectiveC(lines, linesArray.dynamicType)

Here, I declared a Swift array and set it equal to the bridged CFArray. This compiles without error, but when I run it, I get an EXC_BREAKPOINT crash on the second line. Perhaps I'm not using the Swift language correctly on this one...

回答1:

Here is how to do this, based on the current state of the Swift compiler and Swift documentation. Hopefully this gets cleaned up in later betas.

UPDATE: Since Beta 5, reinterpretCast has been renamed to unsafeBitCast, and a CTLine object must be sent to it as an input. Way #2 still does not work.


Way #1 - Use the CFArray directly

let line: CTLine = reinterpretCast(CFArrayGetValueAtIndex(lines, 0))

Regarding Gary Makin's comments - The Ref can be dropped from CTLineRef, but this does not change ARC vs non-ARC. According to Using Swift with Cocoa and Objective-C pages 53-54, ref and non-ref are identical to the compiler. Attempting to call CFRelease causes a compiler error.


Way #2 - Convert the CFArray to a Swift array - Does not currently work

Ideally, we want to convert lines to a Swift array of CTLine objects since we know that's what is returned by CTFrameGetLines, giving us type safety after the conversion. Probably due to a compiler bug, the array can be converted to an [AnyObject] array, but not to [CTLine]. According to Apple's documentation, this should work:

let linesNS: NSArray  = CTFrameGetLines(frame)
let linesAO: [AnyObject] = linesNS as [AnyObject]
let lines: [CTLine] = linesAO as [CTLine]

This converts CFArray to NSArray, then NSArray to Swift Array [AnyObject], then downcasts that array to the specific type CTLine. This compiles, but when it is run, there is an EXC_BREAKPOINT crash on the last line.



回答2:

Apparently in Swift 2 you can cast CFArray as [AnyObject].

I spent a lot of time figuring out why my code stopped working after converting from Swift 1.2. In my case it turned out that some API changed from CFArray! to CFArray? and this cast was returning nil:

let cfArray = ... // Function that returns CFArray? instead of CFArray!
if let array = cfArray as? [AnyObject] { // nil
...

There's no warning that I'm optionally casting optional to non-optional, which makes no sense.



回答3:

As of Swift 3, CFArray can be bridged to [CTLine] directly:

let lines = CTFrameGetLines(frame) as! [CTLine]
guard let firstLine = lines.first else {
    return // no line
}

And in the same way:

let runs = CTLineGetGlyphRuns(firstLine) as! [CTRun]
guard let firstRun = runs.first else {
    return // no run
}

(tested with Swift 3.1/Xcode 8.3.3 and Swift 4.0/Xcode 9).



回答4:

based on answer from purrrminator:

extension CFArray: SequenceType {
    public func generate() -> AnyGenerator<AnyObject> {
        var index = -1
        let maxIndex = CFArrayGetCount(self)
        return anyGenerator{
            guard ++index < maxIndex else {
                return nil
            }
            let unmanagedObject: UnsafePointer<Void> = CFArrayGetValueAtIndex(self, index)
            let rec = unsafeBitCast(unmanagedObject, AnyObject.self)
            return rec
        }
    }
}

let cfarray = giveMeArray()

for entry in cfarray {
    print(entry)
}


回答5:

Just tried in XCode 7.3.1 (Swift 2.2.1)

let cfElements = IOHIDDeviceCopyMatchingElements(self.device, nil, IOOptionBits(kIOHIDOptionsTypeNone)).takeUnretainedValue();
let nsElements:NSArray = cfElements
let elements:Array<IOHIDElement> = nsElements as! Array<IOHIDElement>

for element in elements { /**/ }

I'm not sure if the intermediary cast to NSArray is necessary, but that'll do for me for now