Shimming SwiftUI APIs by Hacking Overload Resolution
The best way to build an app is with Swift and SwiftUI – if you don’t have to support older iOS versions. But it doesn’t have to be like this – with one weird little trick.
Motivation
Because Swift doesn’t support back deploying types and only recently implemented SE-0376 Function Back Deployment, SwiftUI as a system framework can only introduce new features for the latest iOS version. As KDE Connect iOS needs to support iOS 14, if we want to use some of the newer SwiftUI APIs, we have to check what’s the iOS version of the current device, then execute different branches of code depending on what API is available:
1 | if #available(iOS 15, *) { |
Not only do we have to add the if #available
checks, to reduce code duplication, we need to introduce variables like theSameActualViewContent
, title
, and primaryButton
. If we have multiple alerts, we have to repeat this process for every single one of them. The friction makes writing SwiftUI not fun anymore.
Proposed Solution
One way to solve this is to “bring the new APIs to an older environment, using only the means of that environment,” or implement what people call a “shim.”
Other common terms that describes this are “backport” and “polyfill.”
While libraries like SwiftUI Backports exists, there are still times that we need to do this ourselves if the library hasn’t gotten to the thing we want, such as support for @FocusState
on iOS 14. In addition, to make it easier to later drop support for older iOS versions, it’s possible – and the easiest – for in house wrapper/implementation to utilize overload resolution to keep the shim API exactly the same as SwiftUI at call sites. This means, instead of needing to add some disambiguator like in this blog post:
1 | FocusState var isFocused . |
or using a different name such as in this YouTube video and many other online tutorials:
1 | var isFocused |
the goal is to directly write:
1 | var isFocused |
as if the shim doesn’t exist.
Overload Resolution
When multiple types, functions, and/or variables from different modules share the same name X
, the compiler needs to figure out which one exactly are you referring to when you write X
in source code. According to Swift’s documentation on resolving name lookup ambiguities:
- Declarations in the current source file are best.
- Declarations from other files in the same module are better than declarations from imports.
- Declarations from selective imports are better than declarations from non-selective imports. (This may be used to give priority to a particular module for a given name.)
- Every source file implicitly imports the core standard library as a non-selective import.
- If the name refers to a function, normal overload resolution may resolve ambiguities.
That means we could provide shims by declaring, for example:
- a type alias called
FocusState
to shadow SwiftUI’s definition ofFocusState
type
1 | typealias FocusState = State |
- an “overload” global function that’s similar to SwiftUI’s definition of
Button.init
initializer but with custom types available on iOS 14
1 | func Button( |
- an overload function called
refreshable
with a slightly different signature but indistinguishable from call site if using trailing closures
1 | extension View { |
Since these are “Declarations from other files in the same module,” they are “better” than declarations imported from the SwiftUI framework. As they are available on iOS 14, the compiler will happy take these shims over the actual SwiftUI APIs.
Migration
What needs to happen when we drop support for iOS 14? Just delete the files implementing the shims. To make sure we remember doing this, we can mark the shim APIs to be obsolete by the iOS version they become available at:
1 | @available(iOS, obsoleted: 15, |
Since the backport implementation is different from SwiftUI, always test the app to make sure other parts of the code base are not relying on shim-specific behaviors.
Further Readings
But how do you implement the new alert API on iOS 14? Find out more at Backporting SwiftUI APIs with Result Builders.