Why is @available property wrapper allowed but stored property isn't?

I ran into this issue when working with @FocusState from SwiftUI, but as a general Swift question:

Why is it possible to add an @available marker to a property using a property wrapper, but not a stored property of that same type?

Here's a simple example from an Xcode 13.3 playground:

1 Like

It's actually a bug fixed on the main branch. Conditionally-available property wrappers are not allowed for the same reason as conditionally-available stored properties.

1 Like

Does that mean in the next release of Swift existing code that uses

@available(iOS 15.0, *)
@FocusState private var isFocused: Bool

won't compile anymore?

Yes, since FocusState is itself only available on iOS 15+, this code never should've compiled unless your app's deployment target, or the type the stored property appears in, is iOS 15+.

A correct implementation of this requires doing some form of dynamic layout where the stored property is omitted if the type metadata for it is not available. This would also require language changes to definite initialization to ensure that the compiler could handle version checks in initializers. It's possible it will be implemented in the future, but it is not implemented today.

5 Likes

Thanks for the response!

I hadn't gotten far enough to test the code I was writing on a pre-iOS 15 device, but based on this discussion thread it sounds like it would have crashed at runtime. So even though the compiler allowed it previously there shouldn't be any apps using that "feature"(bug)

https://developer.apple.com/forums/thread/688678

1 Like

I can’t promise it will crash every time, but yeah.

1 Like

I'd be interested to understand why we can't use @available on stored properties without a property wrapper. I often find myself writing code like this:

struct Thing {

  // What I wanted to write:
  // @available(iOS 10, *)
  // var feedbackGenerator: UISelectionFeedbackGenerator? = nil

  // What I had to do instead:
  private var _feedbackGenerator: Any?

  @available(iOS 10, *)
  var feedbackGenerator: UISelectionFeedbackGenerator? {
    get { return _feedbackGenerator as? UISelectionFeedbackGenerator }
    set { _feedbackGenerator = newValue }
  }

  init() {
    if #available(iOS 10, *) {
      feedbackGenerator = UISelectionFeedbackGenerator()
    }
  }
}

This moves the @available declaration from the stored property to a computed property, and then everything is happy. But it feels like a bunch of unnecessary.

I (being naive) would assume that when running on older OS versions, the compiler could just fill the memory of that property with 0s, and then the compiler would disallow all access to that property. Would something like that feasible?

3 Likes

The size of a type isn't necessarily known at compile time in Swift. That's why in the fully general case you'd need a fallback like Slava described, handling a fully or partially invalid type (consider Result<OnlyOnNewOS, Error>) without crashing. In theory the compiler could special-case class references, or optional class references, or any type with fully-known layout (recursively frozen on all structs and enums, basically), and in those cases "fill with zeros and disallow all access" would work. But the rule might be hard to explain. Either way there needs to be some work done on the compiler and possibly the runtime as well, but it's not impossible.

4 Likes

You've replaced the storage of the type with 'Any', which adds a layer of dynamic boxing around the actual value (UISelectionFeedbackGenerator). Older runtimes know how to lay out an 'Any', but not necessarily 'UISelectionFeedbackGenerator'.

2 Likes

This is a slightly different issue, but previous to Swift 5.7, you could use @available on lazy properties but now it gives the same error about using @available on stored properties. Was this an intentional change or a bug?

https://github.com/apple/swift/pull/41112

Thanks, good to know it was always an issue.