It’s Xcode version switching season for us app developers.
When getting ready to support the next version of iOS we need to switch between Xcode betas and release quite a lot.
One thing that would simplify this is if the @available(iOS 13.0, *) annotations were forward compatible from the released Xcode with an SDK that doesn’t support iOS 13 by ignoring the function/block entirely.
(Ignoring a function should IMHO produce a warning by the compiler, as it’s not completely obvious that the function/block is being removed.)
Usually I’ve been working in teams with a feature-branch (feature/ios13) which gets merged when the GM is released.
The proposed functionality would only solve a subset of the issues that arise here - but IMHO it’d be a step in the right direction.
That‘s true, but that would also introduce more complexity.
The @available notation is already required in many cases to achieve backward compatibility (e.g. for new delegate methods accessing new properties). I’m suggesting that it also provides forward compatibility, so we can go back and use an older compiler without using more code “annotations”.
There’s already work done here at compile time to make sure that we don’t access properties we shouldn’t (there are e.g. very helpful errors when accessing new properties). So naively it should be possible to remove blocks too.
I ran into a similar problem in Xcode 11 on an app that supports iOS 9; if you build for a 32-bit platform, the compiler can’t see any of the SwiftUI symbols (yes, I know SwiftUI is not backwards-compatible, but the rest of the app doesn’t use it). Even gating the file with #if canImport(SwiftUI) doesn’t fix it, so this is my current solution:
If @available is a runtime check, that means the compiler has to know how to compile that code and then the runtime simply doesn't run it when in an incompatible environment.
How is the Swift 4.0 compiler to compile Swift 4.1 code? Isn't that exactly what compiler macros are for?
You get errors when not guarding statements marked as available in a later runtime/SDK than your minimum deployment target (e.g. you get compile-time errors for using iOS 13 APIs when your minimum target is iOS 12).
Yes, the inserted guards are runtime-based, but the keyword and semantics are also there and used at compile time. Currently that part is backward compatible (supports iOS 12 when compiling with the iOS 13 SDK), but not forwards compatible (ignores iOS 13 when compiling for iOS 12 SDK).
So this has nothing really to do with Swift versions and only with the SDK, runtime and minimum deployment target.
Edit:
To try to clarify what I’m proposing. The compiler should know what version of the iOS SDK it is currently compiling against.
If there are annotations with a higher version than the current one - it should understand that it cannot generate code for that block, and just ignore it (+a warning) - instead of trying to generate code for it.
The compiler will still be able to parse the ignored block (though not infer types and generate code etc.).
if @available(iOS 14.0, *) {
// uses an iOS 14 object
}
and
@available(iOS 14.0, *) var object: NewTypeIn14
These blocks ought be ignored when compiling against the iOS 13 SDK. And, without really knowing any better, the infrastructure around it should already be in place. Right?
What you're wanting is what compiler preprocessor macros are for
My understanding is the errors seen with @available are from a compile time check, but the code (once the error is fixed) is still compiled and included in your binary then left it up to the runtime whether it should execute the code.
That means the compiler you're using must know how to compile @available code which means, as it exists today, forward compatibility is impossible.
In summary:
If you need to change what code runs in an environment, use @available.
If you need to change what's compiled, use compiler macros.
I do agree that it would be nice to have something that handles both! However Compiler macros and run-time checks will likely always be needed, so it may be necessary to add something different altogether.
I could see an additional parameter added to @available, but whether that's possible/practical depends on how that features is built, which I can't speak to.
There’s no such thing as a ‘preprocessor macro’ in Swift.
AFAIK it was an intentional design goal not to include anything preprocessor due to the wide range of problems and complexity it introduces for e.g. the parser.
In the later proposal related to conditional Swift version they do use the term ‘check’ (just like you did about ‘@available’).
And I’m not really sure where it’s documented that ‘#’-prefix is only used before code-generation (in Swift)?
It is breaking a convention, yes - but IMHO it’d be worth it (it would solve many of the weird problems we have 2-4 months/year and must work around). IMHO it’s clear enough not to cause any real problems.
A conditional compilation block allows code to be conditionally compiled depending on the value of one or more compilation conditions.
I'm unsure whether # is always pre code generation, but if you want to compile different things for different compilers, a Conditional Compilation Block is the thing purpose built to do just that.
Declaration Attributes have some similar use cases, but semantically are markedly different:
Apply this attribute to indicate a declaration’s life cycle relative to certain Swift language versions or certain platforms and operating system versions.
@available is an attribute to specify function or class version support and is handled at compile time. The runtime check is performed using if #available() { } construct.
I realize that my initial explanation wasn’t very good, and not really properly digested by me before posting it. Sorry about that.
I do not want to compile different things for different compilers.
I want some things to not get compiled, depending on the version of Apple’s SDKs that is being linked to. I want the compiler to ignore code that it doesn’t yet support on a framework version level.
This issue is highly Apple specific as it depends on their forward compatibility of the frameworks. Code written for iOS 12 will (generally) also “work” out-if-the-box when linked to the iOS 13 SDK (as in it produces a runable result that likely needs tweaking) - I’d like this to work backwards too.
@available(iOS 13.0)
func myModernFunction() {
// code that use new SDK functions
}
when compiled using older SDK be completely ignores by the compiler. What I don't get yet, is how are you supposed to use that code. As new code is isolated using runtime check, you can't just remove the function and expect the calling site to work.
Can you write a simple example of what you want to write, and what you expect the compiler generate/compile ?
The calling site also needs to be guarded in an if @available, so both would get stripped.
I’ll see if I can get around to really think this through and structure it in a better way.
I posted it to get some general feedback on it, and if there was any interest in such a solution - but it doesn’t seem so positive, so I’m not sure I’ll get around to really digest the idea and present it properly.
It is not guard by a if @available(), it is guard by a if #available().
And the #available directive is very different. It is use to generate a runtime check of the system version.
Nothing prevent you to use a futur OS release in a #available() check. This is perfectly valid to write things like this with current SDK:
if #available(iOS 45, *) {
// do whatever you want.
} else {
// Do it the old way.
}
This runtime check purpose it to allow executing code that should run on newer platforms than the deployment target. The compiler has no reason, an no right to strip that code, as it can't know if your app will run on such target.
If you want positive feedback, I think you should take more time to think about what you need and really try to write some code that need this, and how it should be interpreted. And also how it would impact existing code.
Without more details, people will not be able to understand what this proposition is exactly, and will end up trying to guess and emit doubt about it.
I think what's desired here is something like this:
#if sdk(iOS 45)
// do whatever you want.
#else
// Do it the old way.
#endif
Although in practice such a construct would be used in this way:
#if sdk(iOS 45) // compile time check
if #available(iOS 45, *) { // runtime check
// do whatever you want.
} else {
// Do it the old way.
}
#else
// Do it the old way.
#endif
Since both else clauses do the same thing (the old way), it'd be desirable to have a way to merge them.
Of course the #if sdk(...) thing doesn't exist yet either. You have to use #if canImport(...) to test a module name that was added in that particular SDK version.
Though there are also compile-time checks in #available-protected blocks - as we get warnings about using methods not supported on older iOS versions that we still support.
I’ll get back to the drawing board and present it better. Thank you for your feedback.