Support Negative Availability Literals

It's currently not possible to do negative version checks:

    if #available(iOS 13, *) == false { ... }
    if !#available(iOS 13, *) { ... }

If you need to, your option is either to do a guard check or to put an else right after the condition:

    guard #available(iOS 13, *) else { ... }
    if #available(iOS 13, *) {} else { ... }

Negative availability checks are important when an API is completely different across versions. In the case of iOS apps that support Scenes, you need a negative availability check in your AppDelegate to account for the fact that UIWindows should be loaded elsewhere in iOS 13+. The current way to do this unfortunately is to do one of the uglier workarounds:

    // if NOT in iOS 13, load the window.
    // Post iOS 13, the window is loaded in the SceneDelegate.
    if #available(iOS 13, *) {} else {
        loadMainWindow()
    }

To enable this, I pitch to improve availability literals to support the negation methods shown in the beginning. Originally I pitched to make them usable as booleans in the shape of a new ExpressibleByAvailabilityLiteral, but that would ruin the purpose of this literal which is to prevent you from calling things you shouldn't have access to. In that sense, I think that just expanding it to support negation would be a win.

34 Likes

Big +1 from here.

We have lots of these in empty if-blocks with an else block in our codebase.

1 Like

+1

I sometime wished I had this, but I also appreciate the current limitation that forces things in the order from the most recent behavior to the oldest one. Consider this:

if #available(macOS 11, *) {
   // nothing to do!
} else if #available(macOS 10.14, *) {
   // do something
} else {
   // old behavior
}

We could allow negation. Then a new OS comes in or you make some changes and you have to modify the behavior again. It becomes a bit hard to track which branch applies to which OS:

if !#available(macOS 10.14, *) {
   // old behavior
} else if !#available(macOS 11, *) {
   // do something
} else {
   // nothing to do!
}

It's nice that you can remove the final else. But at this point you probably should refactor by removing the negation because it's not clear what happens on which OS.


Personally, I don't mind having to write an empty clause. I usually format it like this:

if #available(macOS 11, *) {
} else {
   // old behavior
}

I'm not opposed to allowing negation though.

Yes please! Also on the Mac side (mysterical new behavior if you touch any xib or storyboard with NSTableView’s anyone?!) I’m currently hitting a ton of places where it would be so much clearer having a negative compatibility check.

And while we’re at it, could we get it to also work with the ? operator please?

For example:

let someMargin = #available(macOS 11, *) ? 10 : 20

Instead of a full blown if statement of 6 lines.

If we think that this is important, the compiler could require that your branches go in availability order, and you could only use !#available if you don't have an else. On the other hand, though, that could interfere with some other reason for ordering the branches.

I didn’t even realize that didn’t work. I’m neutral on this pitch, since I think guard is a reasonable substitute, but I’m +1 on the pitch this isn’t, for adding support for the ternary ?: operator.

Guards work well when the overall logic between different versions are very different, but in the case of SceneDelegate's example, you want everything to be exactly the same except the creation of the window. To use a guard we'd have to abstract it further, which can be done, but I'm thinking this would be a lot easier and cleaner to read if you could simply negate the check.

Exactly guards don’t make sense for a lot of the use-cases for the negative check, especially when it comes to setting up a lot of view state, like configuring a table cell with fonts, margins, image sizes, etc etc. This is also where the ternary operator comes from, it’s not simply a matter of “if nothing is needed on macOS11 just use a guard”, it’s much more when setting up things with specific values for each for a number of properties without a lot of repeating code in two halves of a single if statement, or creating an all or nothing guard.

Just like it feels odd to me that the #available statement acts like a Boolean func that can be used in if, while, guard, but cannot be negated, it also feels weird it can’t be used in a ternary operator.

But just to be sure and not to hijack the core proposal, I already would be a very happy man if I can simply negate the availability check as originally proposed!

My original pitch was actually to allow availability checks to be evaluated as boolean expressions, which would allow using it in ternary operators as well. The issue I found is that I think this would be considerably harder to implement compiler-wise because of situations like this:

let hasiOS13: Bool = #available(iOS 13.0)
if hasiOS13 {
    // It's true, but it's not an availability check. Should this expose iOS 13 APIs?
}

While this is unrelated to the idea of supporting the ternary operator itself, I think that in order to support it we would have to make the above partially possible. It doesn't work right now because the current availability is hardcoded to be usable only in statement expressions, but someone from the core team would be able to provide more info on the feasibility of supporting more than simple negation.

3 Likes

Using availability outside of something that's structurally a condition is Hard, because now your availability errors have to do some kind of conservative dataflow analysis to be emitted, and people would complain about the places where a human can prove that something's safe but the compiler cannot. This gets even worse with transforms like function builders, which may also need to know what's in a limited availability context.

Extending availability checks to ternary operators, on the other hand, would be fine (edit: from a "technically possible" point of view), since the compiler can see their structure up front. (Although since function builders work on a statement level, you probably wouldn't be able to get the special handling of limited-availability sub-objects that you get with if #available.) @mekentosj, I'd love to see some more real-world use cases where you would have used a ternary operator instead of if but couldn't.

I think you might have misunderstood, I wasn't saying that it would not be possible to use if, it's simply convenience. Just like there is no reason why you couldn't use:

if #available(iOS 13, *) {
    // do nothing
} else { 
    // do my thing
}

instead of the proposed:

if !#available(iOS 13, *) {
    // do my thing
} 

It's simply a matter of style/convenience/cleanness.

Here's an example:

// some view configuration code
myCell.leadingEdgeConstraint.constant = #available(iOS 13, *) ? 10 : 15
myCell.trailingEdgeConstraint.constant = #available(iOS 13, *) ? 10 : 15
myCell.topEdgeConstraint.constant = #available(iOS 13, *) ? 5 : 20
myCell.bottomEdgeConstraint.constant = #available(iOS 13, *) ? 5 : 20

instead of

// some view configuration code
if #available(iOS 13, *) {
    myCell.leadingEdgeConstraint.constant = 10
    myCell.trailingEdgeConstraint.constant = 10
    myCell.topEdgeConstraint.constant = 5
    myCell.bottomEdgeConstraint.constant =  5
} else {
    myCell.leadingEdgeConstraint.constant = 15
    myCell.trailingEdgeConstraint.constant = 15
    myCell.topEdgeConstraint.constant = 20
    myCell.bottomEdgeConstraint.constant = 20
}

Or alternatively as @rockbruno mentioned, would be great if you could assign it to a variable so you can use that again in a ternary setup as above, which would be even easier to read.

:-) I think you misread my sentence: I said "would have used ?: but couldn't". Metrics are a reasonable example, and they're also a case where you're using #available to check the behavior of the OS rather than the availability of specific APIs, so you don't need the compiler's extra enforcement. I'm curious if there are any non-metrics use cases—i.e. differences that aren't going to just be "different constants on different OSs"—but maybe those cases aren't ones where you'd use a ternary operator even if it were allowed, because the subexpressions get too complicated.

You are right, apologies.

Could very well be indeed. To give some context, I just chimed in based on the Swift Weekly newsletter mentioning this topic where upon reading it I was like "YES!" because it happens to be that time of the year again where trying to keep your app working nicely on older OS's indeed requires a slew of places where we need to pixel and functionally tweak things. Of the dozens of #availability checks I had to add I'd say it really is about half the time where I would have preferred to use the negative or a ternary variant as shown above, meaning that half the checks have currently introduced a bunch of unnecessary, ––or unnecessarily verbose– code.

2 Likes

Something like this maybe?

layer.borderColor = (#available(iOS 13, *) ? color.resolvedColor(with: traitCollection) : color).CGColor
3 Likes

Adopting dark mode requires a lot of code like:

if #available(iOS 13.0, *) {
  view.backgroundColor = .systemBackground
} else {
  view.backgroundColor = .white
}

where a ternary would have felt natural.

3 Likes

Yup, and a good example where it looks similar to metrics but where you would need in fact the compiler check for some colors, materials, etc to not be available on older systems. Other things that come to mind are enum and optionset values newly introduced in new major OS releases.

Yes, please! This is such a welcome change if possible. It really feels unnatural to write checks like:

if #available(iOS 13, *) { } else {
   // old behavior for iOS 12 and lower
}

Maybe, thinking out loud, a syntax like this works too:

if #available(iOS <13, *) {
   // old behavior for iOS 12 and lower
}

Although the ! aligns best with boolean operators which we're used to.

1 Like

I like the idea of supporting ternary operators, but from what I have been seeing enabling this in the compiler appears to be considerably harder than the basic negation. I thought of rephrasing this pitch as "Expand Availability Literals" so we could bundle multiple improvements together, but since one is easier to implement than the other I think dividing this into two proposals may be the better choice (unless someone wants to co-implement this! :slight_smile:)

1 Like

I was able to hack a quick prototype for this: https://github.com/rockbruno/swift/commit/f870fd3d5601071ac7cc0f67a6c352e3398f0649

The idea is that the compiler is already able to calculate the "false" block, so we simply swap the refinement contexts if the availability is negative.

It's hard to test this without building the toolchain and trying it out with iOS versions, but the following snippet:

if #available(OSX 999, *) {
	print("wrong")
} else {
	print("right")
	if #available(OSX 999, *) == false {
		print("right")
		if #available(OSX 999, *) {
			print("wrong")
		} else {
			print("right")
		}
	} else {
		print("wrong")
	}
}

correctly prints "rightrightright". I however need to check with a toolchain in Xcode if the else blocks are correctly restricting/allowing versions, and if function builders need to be modified to adopt this.

Terms of Service

Privacy Policy

Cookie Policy