If #available check failing

I had to enable the new isInspectable property to allow remote inspection of WKWebViews (added in iOS 16.4). I'm using Xcode 14.3 (sdk 16.4) but we have other developers still using Xcode 14.2 (and older). 14.2 ships with sdk 16.2.

I added a debug only availability check to only set the property if running on 16.4 or later. It looks like:

// Enable debug inspection of webview
#if DEBUG
if #available(iOS 16.4, *) {
    self.isInspectable = true
}
#endif

This check works fine when using Xcode 14.3 building for a 16.4 or 16.2 simulator. However, when building with Xcode 14.2 for a 16.2 simulator, the check fails (returns true) and I get a build exception.

What am I not seeing? I expect the check to work (return false) any time running against a version lower than 16.4 -- but it seems it fails here because the compiler can't find the symbol (value of type has no member 'isInspectable'). Isn't this the exact purpose of the #available check?

I have verified that I'm using the correct command line tools with the respective Xcode version (using xcode-select -s and also checking Settings->Locations->Command Line Tools)

I had thought that the check worked like Objective C's "respondsToSelector" which would work regardless of the build / target sdk. But it seems like #available does not guard against building with an sdk version where those symbols don't exist. ie it's a run-time vs. a build time check.

2 Likes

I had thought that the check worked like Objective C's "respondsToSelector" which would work regardless of the build / target sdk. But it seems like #available does not guard against building with an sdk version where those symbols don't exist. ie it's a run-time vs. a build time check.

Yes, if #available is only a runtime check; it doesn't change the fact that the compiler still needs to type check the code inside the then block. When building against the 16.2 SDK, there's no property declaration that the compiler can use to understand how to compile the statement self.isInspectable = true. Even in Objective-C, a respondsToSelector check wouldn't work on its own; you would also have to forward declare the isInspectable property in your project so that the compiler has a definition of the selector for the getter. There is no equivalent to forward declaration in Swift (at least for arbitrary kinds of declarations).

There have been some previous discussions of language features that could be added to Swift to address this problem, mostly revolving around allowing developers to conditionalize code at build time based on either the version of the SDK or the version of a specific module.

2 Likes

Thank you! I had sort of reasoned my way to that. I did just find what I think is a solution (verifying now) but seems a terrible hack.

// Enable debug inspection of webview
#if compiler(>=5.8) // Xcode 14.3
#if os(iOS)
#if DEBUG
if #available(iOS 16.4, *) {
    self.isInspectable = true
}
#endif
#endif
#endif

...or...

#if compiler(>=5.8) && os(iOS) && DEBUG
if #available(iOS 16.4, *) {
    self.isInspectable = true
}
#endif
1 Like

Isn't os(iOS) unnecessary though? You could just add macOS 13.3 to the #available(..., *) check if targeting more than iOS:

#if compiler(>=5.8) && DEBUG
if #available(iOS 16.4, macOS 13.3, *) {
    self.isInspectable = true
}
#endif

I agree it's unfortunate that compile-time conditions like DEBUG can't be included in structural conditional statements like if or guard and always require the syntactically heavy #if to be used. It would be nice if the above could be written something like

if #compiler(>=5.8), #DEBUG, #available(iOS 16.4, macOS 13.3, *) {
    self.isInspectable = true
}

so that compiler versions before 5.8 would stop checking API availability after the #compiler condition, for example.

But this would be just a syntactic improvement with somewhat more limited utility than #if, so the bar for introducing it is fairly high.

2 Likes