compile CaseIterable in Xcode 9 but not in Xcode 10

Can anybody tell me a clean way to compile a Swift protocol and its implementation in Xcode 9 but not in Xcode 10?


I tried various options, including some based on conditional compilation like #if swift(>=4.2), and currently use a very ugly method based on duplicating the target. I wonder if there isn't any clean way.


The concrete reason is the protocol CaseIterable. I welcome the addition of this protocol to Swift 4.2 in Xcode 10, as it enables iterations over all cases of an enumeration. I look forward to removing my own implementation of that protocol, which was based on the fact that hashValue and rawValue in an enumeration used to return the same thing (described at various places, in this blog for example). This implementation does not work in Xcode 10 anymore, since hashValue has changed. That should not be a problem, I can just use the new compiler-supported implementation of CaseIterable, right? Unfortunately I cannot see a clean way of excluding my implementation of the protocol in Xcode 10 while keeping it for Xcode 9.

Accepted Reply

cp101 – See if checking against (>=4.1.50) helps: vapor/core/pull/160/files at github

(link deconstructed to avoid sitting in moderation queue)


"Swift 4.2 compiler defines

CaseIterable
even if it is compiling in a compatibility mode. This PR uses a hack that the Swift team has given us until the
#compiler
directive is available to determine whether or not we are using the Swift 4.2 compiler in lieu of just checking the version."

Replies

Can you explain more about why

swift(>=4.2)
approach isn’t working for you. I played around with this here in my office and it seems to work for me, so I must be missing some subtlety here.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Thank you for asking. I left this excursion out because it diverted from the original issue, but I can gladly show you the three compiler bugs that make it impossible to negate swift(>=4.2).


The correct way would be to compile the fragment only for versions of Swift below 4.2, or swift(<4.2):

#if swift(<4.2)
public protocol CaseIterable: Hashable {
...
                guard current.hashValue == raw else {
...
#endif

Unfortunately, the compiler does not recognize "<" as a unary comparison and fails with "Unexpected platform condition argument: expected a unary comparison, such as '>=2.2'" (both in Xcode 10.0 beta and in Xcode 9.4.1).


Under the assumption that there will not be any intermediary versions (such as 4.1.1), it would be sufficient to check for below or equal to version 4.1, or swift(<=4.1):

#if swift(<=4.1)
public protocol CaseIterable: Hashable {
...
                guard current.hashValue == raw else {
...
#endif

No luck, though, both Xcode 10.0 beta and in Xcode 9.4.1 still don't like "<=" and fail with "Unexpected platform condition argument: expected a unary comparison, such as '>=2.2'".


Finally, we could stick with the only accepted unary comparision ">=" and put the code into the alternate branch after a swift(>=4.2):

#if swift(>=4.2)
#else
public protocol CaseIterable: Hashable {
...
                guard current.hashValue == raw else {
...
#endif

This code fragment is accepted by both versions of Xcode. Unfortunately, Xcode 10.0 beta ignores the #else statement and compiles the code after #else, anyway. Now the compiler replaces the built-in protocol CaseIterable with the implementation intended for previous versions of Swift, and creates wrong code (since hashValue and rawValue differ).

Did you try a fourth solution:


#if swift( =4.2)

#else

#endif

#if swift(=4.2) as well #If swift( =4.2) (as you wrote it) both fail with "'=' must have consistent whitespace on both sides".


#if swift( = 4.2) fails with "Expected expression in list of expressions".


#if swift( == 4.2) fails with "Unary operator cannot be separated from its operand".


#if swift(==4.2) and #if swift( ==4.2) both fail with "Unexpected platform condition argument: expected a unary comparison, such as '>=2.2'", as before.

I tried the following in 9.2 (Swift 4.0) ; I cannot presently test with XCode 10.


        #if swift(>=4.2)
            print("swift(>=4.2)")
        #else
            print("swift(<4.2)")
        #endif
        #if swift(>=4.1)
            print("swift(>=4.1)")
        #else
            print("swift(<4.1)")
        #endif
        #if swift(>=4.0)
            print("swift(>=4.0)")
        #else
            print("swift(<4.0)")
        #endif


And got expected result:

swift(<4.2)

swift(<4.1)

swift(>=4.0)


So if that doesn't work in XCode10, looks like a bug in XCode 10.

Unfortunately, Xcode 10.0 beta ignores the #else statement and compiles the code after #else, anyway.

It does? If so, that’s definitely a bug and I’d recommend that you file a bug report about it. However, it seems to work in my testing. Here’s what I did:

  1. In Xcode 10.0b1, I created a new project from the Command Line Tool template.

  2. I replaced the contents of

    main.swift
    with the following:
    #if swift(>=4.2)
        print("high")
    #else
        print("low")
    #endif

    .

  3. I built and ran it. The template hasn’t been updated to Swift 4.2, so I get Swift 4.1, and it prints

    low
    .
  4. I updated the project to Swift 4.2. Now it prints

    high
    .

What am I doing wrong here?

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

This advances my understanding, but does not lead to a solution. Look at the following extract of your code:

#if swift(>=4.2)
print("swift(>=4.2)")
#else
print("swift(<4.2)")  
#endif

In Xcode 10.0 beta, this leads to "swift(>=4.2)" in a playground or a newly created project, but to "swift(<4.2)" in my project that is compatible with Xcode 9.4.1. This is because the Swift language version is controlled in a target build setting "Swift Language Setting" (or "SWIFT_VERSION" in the actual file). It can be set to Swift 3, Swift 4, or Swift 4.2. In my project, it has to be set to Swift 4 to keep compatibility to Xcode 9.4.1.


To come back to the original issue, the problem is that Xcode 10.0 beta has changed the implementation of the hashValue of an enumeration regardless of the Swift version. Musing on conditional compilation based on the Swift version is pointless. I need to distinguish between Xcode versions to maintain CaseIterable code that works both with both Xcode 9 and Xcode 10. This could look like the following:

#if xcode(>10.0)
#else
public protocol CaseIterable: Hashable {
...
                guard current.hashValue == raw else {
...
#endif

Here the compiler fails with "Unexpected platform condition (expected 'os', 'arch', or 'swift')".

Distinguishing on the version of Swift does not help. Xcode 10.0 beta has changed the implementation of hashValue for an enumeration regardless of the Swift version. What I need is something like #if xcode(>=10.0) #else.


See my explanation on my reply to Claude31 on the other subthread. Apparently we posted at about the same time.

Xcode 10.0 beta has changed the implementation of

hashValue
for an enumeration regardless of the Swift version.

OK. Ignoring the conditional compilation side of things for the moment, do you have an implementation of

CaseIterable
that’s compatible with this new
hashValue
? My understanding was that Swift now mixes randomness into the default hash function (per SE-0206), which is going to make things hard.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Commenting out my own definition and implementation of CaseIterable does work in Xcode 10.0 beta. In that case the compiler just uses its built-in version of CaseIterable based on SE-0194.


The issue is how to include a working implementation of CaseIterable only when the compiler does not provide one. I will need to use both Xcode 9.4.1 and beta versions of Xcode 10 in parallel over the next few months, the former to submit builds to the App Store and the latter to prepare for compatibility with iOS 12.


The SE-0194 proposal does list four alternative workarounds, with their drawbacks. I based my implementation on the only one that worked without explicit knowledge of at least the last case, which was the one based on hash values.

You may be out of luck here, in regard to your specific request. The issue is that the hash-based solution was never guaranteed to work, and should not really have been used in a shipping application. It could have broken at any time.


In effect, it did just break, not because of CaseIterable, but because of unrelated changes to hashing.


I think your best choice at this point is to remove CaseIterable conformance, and write out an allCases static property manually (and maintain it manually when cases are added). This can be an interim solution that works in all versions of Swift 4 without conditional compliation.


The next best choice is to accept that your project is currently dependent on Xcode 9 (and the version of Swift that comes with it), and not try to move it to Xcode 10 until Xcode 10 is released and you can leave Xcode 9 behind permanently.

You may be right that I won't find a good solution to the issue, but not that it is a matter of bad luck.


Both SE-0194 (CaseIterable) and SE-0206 (Hashable) are enhancements to Swift, from version 4.1 to 4.2. Xcode 10 erroneously applies them to Swift 4.1 as well. The best solution would be that Xcode 10 distinguishes correctly between Swift 4.1 and Swift 4.2. The second best would be conditional compilation based on the version of Xcode. Unfortunately, both are in somebody else's hands.


Enumerating cases manually is error-prone, and a step back. I'll go now with a solution based on naming the last case explicitly, avoiding hashValue, and hope this keeps working until Xcode 10 is out of beta. There is still a risk that it breaks when adding cases, but that risk is a bit lower than with enumerating all cases manually. APIs are never guaranteed to work anyway, I have seen APIs break from one minor release to another.

cp101 – See if checking against (>=4.1.50) helps: vapor/core/pull/160/files at github

(link deconstructed to avoid sitting in moderation queue)


"Swift 4.2 compiler defines

CaseIterable
even if it is compiling in a compatibility mode. This PR uses a hack that the Swift team has given us until the
#compiler
directive is available to determine whether or not we are using the Swift 4.2 compiler in lieu of just checking the version."

Yes, that works, thanks! I would not have guessed this. Xcode 10 neither implements Swift 4.1 nor Swift 4.2 when building with Swift 4.1 as Swift Language Version, so let's call that version 4.1.50. 😉

For what it's worth, the Swift team has indicated through the response to https://bugs.swift.org/browse/SR-8071 that this is as designed.


Essentially, if you're compiling with the Swift 4.2 compiler (which is what ships with Xcode 10), even if you're in compatibility mode you can use `CaseIterable`.


I definitely wasn't expecting it either, so yay!