6

Here's a very simple class (subclass of NSObject) that keeps a list of CGPath objects and appends one CGPath to the array on init:

import Foundation
class MyClass: NSObject {

    var list = [CGPath]();

    init() {
        list.append(CGPathCreateMutable());
    }
}

When trying to use this class:

var instance = MyClass();
println(instance.list.count); // runtime error after adding this line

Yields an ugly crash:

Playground execution failed: error: Execution was interrupted, reason: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0).
The process has been left at the point where it was interrupted, use "thread return -x" to return to the state before expression evaluation.
* thread #1: tid = 0x1251d1, 0x00000001064ce069 libswiftFoundation.dylib`partial apply forwarder for Swift._ContiguousArrayBuffer.(_asCocoaArray <A>(Swift._ContiguousArrayBuffer<A>) -> () -> Swift._CocoaArray).(closure #1) with unmangled suffix "392" + 121, queue = 'com.apple.main-thread', stop reason = EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
  * frame #0: 0x00000001064ce069 libswiftFoundation.dylib`partial apply forwarder for Swift._ContiguousArrayBuffer.(_asCocoaArray <A>(Swift._ContiguousArrayBuffer<A>) -> () -> Swift._CocoaArray).(closure #1) with unmangled suffix "392" + 121
    frame #1: 0x00000001064ce0d8 libswiftFoundation.dylib`partial apply forwarder for reabstraction thunk helper <T_0_0> from @callee_owned (@in T_0_0) -> (@owned Swift.AnyObject) to @callee_owned (@in T_0_0) -> (@out Swift.AnyObject) with unmangled suffix "395" + 56
    frame #2: 0x00000001057bf29a libswift_stdlib_core.dylib`Swift.MapSequenceGenerator.next <A : Swift.Generator, B>(@inout Swift.MapSequenceGenerator<A, B>)() -> Swift.Optional<B> + 554
    frame #3: 0x00000001057bf49a libswift_stdlib_core.dylib`protocol witness for Swift.Generator.next <A : Swift.Generator>(@inout Swift.Generator.Self)() -> Swift.Optional<Swift.Generator.Self.Element> in conformance Swift.MapSequenceGenerator : Swift.Generator + 58
    frame #4: 0x00000001064d8e97 libswiftFoundation.dylib`Swift._copyCollectionToNativeArrayBuffer <A : protocol<Swift._Collection, Swift._Sequence_>>(A) -> Swift._ContiguousArrayBuffer<A.GeneratorType.Element> + 1511
    frame #5: 0x00000001064f1951 libswiftFoundation.dylib`protocol witness for Swift.Sequence.~> @infix <A : Swift.Sequence>(Swift.Sequence.Self.Type)(Swift.Sequence.Self, (Swift._CopyToNativeArrayBuffer, ())) -> Swift._ContiguousArrayBuffer<Swift.Sequence.Self.GeneratorType.Element> in conformance Swift.LazyRandomAccessCollection : Swift.Sequence + 449
    frame #6: 0x00000001064daf7b libswiftFoundation.dylib`Swift.ContiguousArray.map <A>(Swift.ContiguousArray<A>)<B>((A) -> B) -> Swift.ContiguousArray<B> + 1339
    frame #7: 0x00000001064da9cb libswiftFoundation.dylib`Swift._ContiguousArrayBuffer._asCocoaArray <A>(Swift._ContiguousArrayBuffer<A>)() -> Swift._CocoaArray + 475
    frame #8: 0x00000001064ced3e libswiftFoundation.dylib`Swift._ArrayBuffer._asCocoaArray <A>(Swift._ArrayBuffer<A>)() -> Swift._CocoaArray + 78
    frame #9: 0x000000010649f583 libswiftFoundation.dylib`Foundation._convertArrayToNSArray <A>(Swift.Array<A>) -> ObjectiveC.NSArray + 35
    frame #10: 0x000000011163b40e

Frame #9 caught my eye: libswiftFoundation.dylib\`Foundation._convertArrayToNSArray. Why would swift be trying to convert my nice, happy, Swift array into an NSArray?

This issue only occurs when using CFType objects in an array. I can use NSObject subclasses in the array just fine (Ex. [UIBezierPath])

The issue can easily be fixed by not subclassing NSObject, however I want to know what exactly swift is doing to my innocent array. Also, how I can still use NSObject as the base class and have arrays of CFType objects such as CGPath.

It was also pointed out (Thanks, @user102008!) that it doesn't have to be a subclass of NSObject, but the property just has to be declared @objc.

There is some documentation on purpose of using @objc and subclassing an Objective-C class in Swift:

When you define a Swift class that inherits from NSObject or any other Objective-C class, the class is automatically compatible with Objective-C.

However, I am trying the use my Swift class from within Swift. There is no mention of side effects in the documentation of different behavior when subclassing an Objective-C class and using it within Swift. But the documentation also mentions bridging Swift arrays to NSArray:

When you bridge from a Swift array to an NSArray object, the elements in the Swift array must be AnyObject compatible.

And goes on to say:

If an element in a Swift array is not AnyObject compatible, a runtime error occurs when you bridge to an NSArray object.

Hmmmm, CGPath isn't AnyObject compatible, but Swift shouldn't be trying to convert my Swift array into an NSArray.

Community
  • 1
  • 1
Andrew
  • 15,357
  • 6
  • 66
  • 101
  • 1
    by the way: the class doesn't even have to subclass `NSObject`; the property just needs to be declared `@objc` for this problem to occur – user102008 Jul 29 '14 at 20:57
  • "CGPath isn't AnyObject compatible" What makes you say that? It seems compatible for me. – newacct Aug 04 '14 at 07:38
  • @newacct I guess really it's that CGPath isn't NSObject compatible, meaning it can't be in a NSArray. – Andrew Aug 04 '14 at 14:02
  • @SantaClaus: Actually, `CGPath` is `NSObject` compatible at runtime. `(CGPathCreateMutable() as AnyObject) is NSObject` evaluates to true. – newacct Aug 05 '14 at 05:01
  • 1
    Update: This has been fixed in Xcode 6 beta 5 – newacct Aug 05 '14 at 05:02
  • @newacct I'll check it again once I get the latest beta up. – Andrew Aug 05 '14 at 05:19
  • It sounds like they changed something: `If you never import a Swift class in Objective-C code, you don’t need to worry about type compatibility in this case as well.` – Andrew Aug 05 '14 at 16:11

5 Answers5

3

Hmmmm, CGPath isn't AnyObject compatible, but Swift shouldn't be trying to convert my Swift array into an NSArray.

It has to. You said you want it to be ObjC compatible, and ObjC can't handle Swift arrays directly. So it has to convert it to an NSArray.

The short answer is that this is working exactly as documented. Since this is iOS, the solution, however, is trivial. Just switch to UIBezierPath (which is AnyObject compatible) rather than CGPath.

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • So it will (try to) convert it to `NSArray` even when I'm not using it in Objective-C? I'm just calling `println(MyClass().list.count);` in Swift, which doesn't seem like it would be necessary to convert... – Andrew Jul 30 '14 at 15:18
  • 1
    You said you wanted it to be compatible with ObjC. It makes that decision at compile-time. Otherwise it would have to perform a possibly very expensive conversion (that could fail) at runtime everytime you requested it. If you really want something like that, create an objc computed property that converts for you. – Rob Napier Jul 30 '14 at 15:19
  • The key here is to realize that putting @ objc in front (or subclassing from NSObject) says "I want you to store this in a way that is compatible with ObjC and I promise I won't use any features that break that." – Rob Napier Jul 30 '14 at 15:23
  • I might recommend opening a radar, however. You ideally should have gotten a compile error at the `append()` line. – Rob Napier Jul 30 '14 at 15:24
  • I'm not convinced it's storing it as `NSArray` as it seems it is converting it to `NSArray` at runtime, as suggested by `Frame #9` of the stack trace. – Andrew Jul 30 '14 at 15:24
  • I didn't say it was storing it as an NSArray. I said it was storing it in a way compatible with ObjC, and that adds certain restrictions. – Rob Napier Jul 30 '14 at 15:25
  • It's still using an ArrayBuffer under the covers. But in Frame 7, you see where it requires that it be compatible with a "cocoa array" since you asked for that in the type definition. – Rob Napier Jul 30 '14 at 15:27
  • 1
    What's more important is that it crashing in Swift is a blessing. The worst answer would be for it to work in Swift and then break in ObjC (since you obviously wanted it to work in ObjC, or you wouldn't have declared it this way). You want things that are going to fail to fail reliably. You wouldn't want Swift to "try its best" and sometimes succeed and sometimes fail. – Rob Napier Jul 30 '14 at 15:29
  • I've done a little more testing, and the error doesn't occur on initialization of a `MyClass` instance, but only when I try to access `instance.list`. So would that suggest that it is okay storing the array, but once I try to access it, it converts it to `NSArray`? – Andrew Jul 30 '14 at 15:41
  • That matches what you see in the crash log. It would be better if it failed sooner (at compile time ideally), and that's what you might open a radar about. But this all matches the expected behavior from the docs IMO. Don't put non-ObjC-safe things in an ObjC-accesible var. (The other radar is that CGPathRef should be convertible now that it's annotated with CF_IMPLICIT_BRIDGING_ENABLED, but that's a different issue.) – Rob Napier Jul 30 '14 at 16:06
  • Why is `CGPath` not "`AnyObject` compatible"? – newacct Aug 04 '14 at 07:39
  • Almost certainly because it's not an `NSObject`. It's a `CFType`. Since it's been audited for memory management briding, I'd hope they could bridge it to Swift cleanly, though. – Rob Napier Aug 04 '14 at 14:19
  • @RobNapier: `CFType`s are `NSObject`s at runtime. – newacct Aug 05 '14 at 05:00
  • @RobNapier Core Foundation objects are `AnyObject` compatible. E.g. you have always been able to put CF types in an `NSArray` in Objective-C by casting them to `id`. `[myArray addObject:(id)myCGColor]` – Jack Lawrence Aug 05 '14 at 08:47
  • 1
    Is that a documented definition of `AnyObject`? I'm agreeing that that *should* be a definition of `AnyObject`, but I don't know that it currently *is* in Swift. – Rob Napier Aug 05 '14 at 13:28
  • I'd like _somebody_ to update their answer or post a new answer regarding what was happening in Beta 4 _and_ what has changed in Beta 5 to fix it. – Andrew Aug 05 '14 at 16:13
1

All objects in Swift that are compatible with Objective-C, use the full Objective-C runtime. This means that when you make a type that inherits from NSObject (or is defined as Objective-C compatible), its methods and properties use Messaging instead of linking at compile time to your method.

To receive a message, all purely Swift objects, like your array, must be converted to their Objective-C counter-part. This happens regardless of if you are currently using the object in Objective-C because no matter what, it uses messaging.

Therefore, if you make a class that inherits from NSObject, you must assume that all properties can be converted to an Objective-C counterparts. As you said in your question, you can achieve this by using UIBezierPath instead of CGPath.

drewag
  • 93,393
  • 28
  • 139
  • 128
  • 1
    In the Beta 4 compiler, you could eliminate the crash by declaring the property as `final`, which tells Swift not use messaging to access the property. Conversely, the Beta 5 compiler no longer uses messaging to access properties, so it succeeds -- unless you declare the property as `dynamic`, which triggers the crash again. – Jay Lieske Aug 05 '14 at 07:17
0

From "Using Swift with Cocoa and Objective-C"

When you use a Swift class or protocol in Objective-C code, the importer replaces all Swift arrays of any type in imported API with NSArray.

So according to this, the array should not be converted to an NSArray. As it does, I also think you should file a bug report.

In the meantime, you could use the corresponding Cocoa-Class or wrap your CGPath in an extra object.

Rivera
  • 10,792
  • 3
  • 58
  • 102
ShadowLightz
  • 242
  • 1
  • 9
  • I don't see how you can reason that the Swift array shouldn't be converted based on that piece of documentation. I am using my Swift class in other Swift code. – Andrew Aug 02 '14 at 19:49
  • This is what I want to say. It definitely shouldn't be converted. In the Swift compiler there are several bugs left. I guess this is one of them. – ShadowLightz Aug 02 '14 at 21:44
  • That text doesn't constrain what will happen when Swift code calls other Swift code, only when Objective-C code calls Swift code. – Tommy Nov 02 '15 at 13:32
0
import UIKit

class MyClass: NSObject {

    var list = [CGPath]();

    override init() {
        list.append(CGPathCreateMutable());
    }
}

var instance = MyClass()
print(instance.list.count) // 1
user3441734
  • 16,722
  • 2
  • 40
  • 59
-2

It is probably the fact that you didn't import Foundaion, which is where NSObject is:

import Foundation

This is where NSObject is contained (as far as I know). Hope this helped. :)

zaak71
  • 1
  • Nope. This didn't help. I have added it to my question example. Anyways, a "I don't know where class x" is error would likely be a compiler error rather than a runtime error that I am experiencing. – Andrew Jul 29 '14 at 14:34
  • Sorry about that then. I am not as experienced of a programmer, so I probably might make mistakes. :/ – zaak71 Jul 30 '14 at 08:27
  • You're wrong:https://developer.apple.com/library/mac/documentation/Cocoa/reference/Foundation/Classes/nsobject_Class/Reference/Reference.html – Sviatoslav Yakymiv Jul 30 '14 at 14:23
  • 1
    If its proven that the answer doesnt solve the problem it should be deleted. – Mizipzor Jul 30 '14 at 15:01