@warn_unqualified_access
Credits to Matt Young for the tip!
In recent Swift releases, we've seen a surge of new attributes such as @unknown
, @propertyWrapper
, and @main
, to name a few:
while all of these are new and exciting on their own, in this article, let's focus on a lesser-known, older, but equally helpful attribute, @warn_unqualified_access
.
Introduction
Quietly introduced in Swift 2, @warn_unqualified_access
helps us remove ambiguity when calling top-level functions and/or static/instance methods.
Imagine building a new app that has a top-level function, doSomething
:
@main
struct FiveStarsApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
func doSomething() {
print("top-level function")
// ...
}
This example uses SwiftUI, but
@warn_unqualified_access
can be used everywhere, as it's a Swift feature.
Now, one of our views also declares and uses a doSomething
method with the same signature:
struct ContentView: View {
var body: some View {
Button(action: doSomething) {
Text("Tap me")
}
}
func doSomething() {
print("instance method")
// ...
}
}
When the button is tapped, doSomething
is called: but which one? Previous experiences in the same scenario might help us here, and we probably know that the instance method has priority over the top-level function.
This was a quick and simple example, but imagine working on a team and having a view with several more declarations. Maybe a team member wasn't aware of the instance method and meant to call the top-level function instead: how can we prevent such scenarios?
This is where @warn_unqualified_access
comes in: adding this attribute to any method declaration will trigger a warning at the call site when its qualifier is not specified (a.k.a. when the call doesn't specify either the intended instance, class, or module).
Going back to our example, let's add our new attribute to the function declaration:
struct ContentView: View {
var body: some View {
Button(action: doSomething) {
Text("Tap me")
}
}
@warn_unqualified_access
func doSomething() {
print("instance method")
// ...
}
}
Building the project will now trigger a warning on the button's action. It reads: Use of 'doSomething' treated as a reference to instance method in struct 'ContentView'
, and then proposes two actions:
- Use 'self.' to silence this warning
- Use 'FiveStars.' to reference the global function
Even if a team member weren't aware of the instance method at first, this warning would make sure they become aware of it and provide the two possible solutions to fix the warning.
A static method example
For completeness's sake, the following is an example where, instead of an instance method, a class method is declared with the same attribute (note how everything is now static
):
class FSClass {
static func main() {
doSomething() // this will trigger a warning
}
@warn_unqualified_access
static func doSomething() {
print("class method")
/// ...
}
}
Origin
The reason for this attribute goes back to macOS's NSView
, where the Objective-C NSView
's print:
instance method was translated to print(_:)
in Swift:
since Swift.print
is a top-level function, when used within a NSView
subclass, it's lower priority than NSView
's one, meaning that calling print(..)
would not print on the debugger, but instead, it triggered the print dialog, yikes!
This was such an annoying issue that in Xcode 9 the
NSView
's method Swift translation has been renamed toprintView(_:)
, ending the ambiguity once for all.
Real example: min/max
Swift provides min
and max
, two generic top-level functions that take in two Comparable
elements.
Pretty much every Swift's Sequence
variation also provides min
max
methods: to avoid any confusion, all of them have been marked with @warn_unqualified_access
.
Adding @warn_unqualified_access
here helps to avoid disambiguation when we invoke a min
/max
method within any of our own sequences.
SwiftUI
In Impossible SwiftUI views
we've seen how easy it is to create views that compile fine but reliably crash our app:
while preventing such scenarios might be tricky without changing SwiftUI's declarations, we can start from our View
extensions.
Imagine to have a design system, where all Text
instances will use a given Font
and foreground Color
, since this is very common, we've decided to create a View
extension:
extension View {
func textStyle(
_ font: Font? = nil,
foregroundColor: Color = .black
) -> some View {
self
.font(font)
.foregroundColor(foregroundColor)
}
}
However, this extension won't stop us to declare views like the following:
struct ContentView: View {
var body: some View {
Text("Hello, world!")
textStyle() // <-- missing the "."
}
}
struct ContentView: View {
var body: some View {
textStyle() // <-- missing the actual `Text` component
}
}
Which will end up in a guaranteed crash.
If we now add @warn_unqualified_access
to our textStyle
method declaration, this changes, as we now get a warning at compile time:
struct ContentView: View {
var body: some View {
Text("Hello, world!")
textStyle() // Warning: Use of 'textStyle' treated as a reference to instance method in protocol 'View'
}
}
struct ContentView: View {
var body: some View {
textStyle() // Warning: Use of 'textStyle' treated as a reference to instance method in protocol 'View'
}
}
We can still write self.textStyle()
, inhibiting the warning and still ending up in a crash, but most likely, this attribute will help us avoid writing bad declarations by mistake.
Conclusions
In this article, we've explored @warn_unqualified_access
, a little known Swift attribute that can help us write better and clearer Swift code.
Do you use @warn_unqualified_access
? Have you seen any other cool examples? Please let me know!
Thank you for reading, and stay tuned for more articles!