27

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...

jeremywhuff
  • 2,911
  • 3
  • 29
  • 33
  • It appears that using reinterpretCast fixes Way #1 (converts the unsafe pointer to CTLineRef). I would love to get some feedback on #2, however. – jeremywhuff Jul 10 '14 at 19:39
  • 3
    Last I checked, you could generally get from a `CFArray` to a Swift `Array` by double casting thru `NSArray`. (e.g. `let nsArray = CTFrameGetLines(frame) as NSArray; let array = nsArray as [CTLine]`) But I haven't tried with these APIs. – rickster Jul 10 '14 at 19:39
  • Thanks for the quick feedback! As you have it written, it compiles, but has an EXC_BAD_INSTRUCTION crash on the second cast line. If you remove "as [CTLine]", then it works, but the array elements must be reinterpretCast when accessed. – jeremywhuff Jul 10 '14 at 20:06
  • From my understanding and usage, rickster’s suggestion is correct and you might be experiencing a compiler bug. One other comment, you should be able to always drop the "Ref" off the end of the name of a CF type. Doing that allows you to use ARC and not have to explicitly release the objects. See the last portion of this Apple WWDV video [Swift Interoperability In Depth](https://developer.apple.com/videos/wwdc/2014/?id=407). – Gary Makin Jul 12 '14 at 03:53
  • According to [dev forums](https://devforums.apple.com/message/1061792#1061792), this functionality is implemented in 6.1. > All your sample lines compile without errors and work as expected in > the latest Xcode, 6.1 (6A1052c) . > > Interoperability with CF objects is impoving. You may find another > issues, in such case it'd be reported as bug. – Chris Conover Oct 20 '14 at 05:00

7 Answers7

10

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.

jeremywhuff
  • 2,911
  • 3
  • 29
  • 33
  • Thanks for posting your work-around. Did you file a bug on this? I had the same problem as well: http://stackoverflow.com/questions/24519235/swift-extracting-downcasting-cftype-based-coretext-types-in-a-cfarray (note to self, post a more general question) – Chris Conover Oct 19 '14 at 15:08
  • Seems that method 2 works now like: let lines = CTFrameGetLines(frame) as [AnyObject] as! [CTLine] – Denis Feb 10 '16 at 08:49
9

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).

Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
7

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.

pointum
  • 2,987
  • 24
  • 31
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)
}
Vladimir
  • 94
  • 6
3

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

PS: Works with .takeRetainedValue as well.

Marco Luglio
  • 3,503
  • 1
  • 21
  • 27
  • 1
    Yes, this works. Directly casting to to a swift array results in an error but with the in-between cast to NSArray it works fine. Thanks! – codingFriend1 May 26 '16 at 10:46
2

In swift 2.0, I've done the converting from CFArray to Array with following code:

extension Array {
    static func fromCFArray(records : CFArray?) -> Array<Element>? {
        var result: [Element]?
        if let records = records {
            for i in 0..<CFArrayGetCount(records) {
                let unmanagedObject: UnsafePointer<Void> = CFArrayGetValueAtIndex(records, i)
                let rec: Element = unsafeBitCast(unmanagedObject, Element.self)
                if (result == nil){
                    result = [Element]()
                }
                result!.append(rec)
            }
        }
        return result
    }
}

I hope there is a better way.

kas-kad
  • 3,736
  • 1
  • 26
  • 45
1

Swift 5

extension Array {

    init(_ array: CFArray) {
        self = (0..<CFArrayGetCount(array)).map {
            unsafeBitCast(
               CFArrayGetValueAtIndex(array, $0),
               to: Element.self
            ) 
        }
    }
}

To use:

let runs = [CTRun](CTLineGetGlyphRuns(line))
CopperCash
  • 563
  • 5
  • 15