Support Negative Availability Literals

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.

Yeah, the real test is whether the compiler complains about symbol availability, not whether the run-time check is correct. Once the compiler's done checking availability, #available is emitted as just a boolean check.

I might prefer that it's spelled:

if #notavailable(iOS 13, *) { }

or

if #unavailable(iOS 13, *) { }

which are reminiscent of

#ifndef BLAH

Those empty braces needed for the current syntax to work are a wart in need of fixing.

4 Likes

My previous commit wasn't properly applying the version ranges, but this new iteration correctly complains about symbol availability when expected. It's now just missing proper treatment of the ! prefix.

From the implementation, I'm starting to think that using the normal boolean notation might not be the best solution for this. As availability literals can't be used as expressions, some hacking will be necessary to make the ! notation work. I think #unavailable could be a good way to cleanly introduce this change, and probably could be implemented in a way that SwiftUI can use it without adding a new protocol method.

4 Likes

Given that I now have a fully functional solution, I've submitted a draft proposal and implementation PR: https://github.com/apple/swift-evolution/pull/1184

1 Like

One thing I was wondering is that #available and #unavailable are very similar in the sense of reading through the code base, so it may be hard sometimes to spot that is an unavailable instead of an available if we are reading through the code looking for a problem. So one suggestion could be in code coloring use a slightly different color for #unavailable ... not sure if this was already thought or discussed but the point is to make it easier to look at a piece of code and easily spot which is an #available and #unavailable condition.

Maybe that's a stupid idea, but I'll throw it in the room nonetheless.

For this to work, we would need to basically have a (in lack of a better word) generic type, that has the availability requirements as a generic parameter.
Something like the following:

let hasiOS13: AvailabilityBoolean<iOS 13.0> = #available(iOS 13.0) // type could of course be inferred

This could enable an app having a single global variable, which can be used in if or guard statements or in ternary expressions and which could still be statically enforced.
It would probably mean a bit more implementation work, but could also pay off quite well IMHO.

It's a fair point. I wish we could go for the basic #available(...) == false but I don't know if the core team would accept it given that the implementation would be a huge hack. It's still in the table though if they think otherwise.

It would’ve been nice if there was a method you could call, like available(.iOS(14)), and get a Bool back which can used in various places like if or just storing it in some variable. So basically #available is modelled as a regular method call and we can deprecate existing uses of it. It might be a bit of a hack too but it seems more natural to me.

1 Like

Or a Struct/Class that provides this method and perhaps other methods to query the runtime environment. That seems more Swifty to me and better than adding free functions.

Using a hashmark to indicate something that doesn't resemble the C pre-processor doesn't seem that sensible, although I guess we're all used to it by now.

The Struct/Class would resemble UIKit's classes like UIDevice, UIScreen etc. that are Singletons and model aspects of the environment.

1 Like

Folks, while I would love to have #available as an expression I don't think it's really technically feasible to do so. This would likely require refactoring the entire symbol availability system, has many edge cases and doesn't really solve many problems. I would rather not diverge from the original proposal which just includes the ability to negate the current check, which is clearly an oversight in the compiler given that you can do so just fine in Obj-C.

4 Likes
Terms of Service

Privacy Policy

Cookie Policy