Translation's Concurrency Pattern Spells Out the Plank for UIKit
How the new Translation Framework clearly defines the Task/SwiftUI Pattern Apple wants you to use in its case to move away from UIKit
The latest version of Apple’s Translation framework is really amazing (and a welcome first-party alternative to solutions such as MLKit). It offers numerous languages to translate to and from (and the combinations in between) and a few routes for presenting/producing translations:
Presenting a mini translation feature on a sheet (like a Translate App Clip)
Programmatically generating translations (singularly or in batch)
Curiously, the manner in which Apple forces developers to use Translation is strikingly passive aggressive: it can be accessed by SwiftUI only. This has been confirmed by an Apple Engineer who suggests UIKit apps would need to host a SwiftUI View if it would like to access Translation.
There are a few other frameworks that also limit their availability to SwiftUI. Charts certainly comes to mind as a UI feature, while Swift Data and Observation reserve more exclusive functionality to SwiftUI. For a feature like translation, an argument could be made that the result is likely going to be displayed in the UI eventually. But that doesn’t necessarily qualify Translation as a UI framework. And, yet, Apple tightly couples Translation with SwiftUI.
There are strong implications and signals to take from all this, even if a translation feature doesn’t apply to you. In fact, it has some of the strongest messaging regarding Apple’s recent stance on UIKit and SwiftUI.
This article will have two main components. We’ll first walk through Translation and how it can be used at a high level. From there, I’ll offer commentary about Apple’s decision to only offer Translation through SwiftUI and what that translates to for the developer community.
How To Use Translation in SwiftUI
There are two ways to provide translations. The first is to present a translation sheet (as mentioned in the intro, think of it as a kind of Translate App Clip):
public func translationPresentation(isPresented: Binding<Bool>, text: String, attachmentAnchor: PopoverAttachmentAnchor = .rect(.bounds), arrowEdge: Edge = .top, replacementAction: ((String) -> Void)? = nil) -> some View
It’s as simple as providing a stateful boolean to toggle presentation and providing the string to translate. The dialog is robust and allows the user to choose the source and target languages, playback pronunciation, and copy the translation to the pasteboard.
The second way is programmatically using a TranslationSession
. However, it’s a class that has no publicly facing initializers. You must obtain a session through one of two (similar) ViewModifiers:
public func translationTask(_ configuration: TranslationSession.Configuration?, action: @escaping (_ session: TranslationSession) async -> Void) -> some View
public func translationTask(source: Locale.Language? = nil, target: Locale.Language? = nil, action: @escaping (_ session: TranslationSession) async -> Void) -> some View
Both provide a session which can then be used to make asynchronous calls to translate:
// Basic call to translate a string
public func translate(_ string: String) async throws -> TranslationSession.Response
// Allows for a batch of strings to be translated and returned back as a batch (e.g. grocery list)
public func translations(from batch: [TranslationSession.Request]) async throws -> [TranslationSession.Response]
// Allows for a bath of strings to be translated, but returned as each is completed through an AsyncSequence
public func translate(batch: [TranslationSession.Request]) -> TranslationSession.BatchResponse
It’s worth noting that you can complete a session all at once and be done:
.translationTask { session in
do {
try await session.translate("This is a test")
} catch {
// catch me!
}
}
Or pass the session into a function:
.translationTask { session in
self.viewModel.translateSomething(using: session)
}
Or pass the session to be held and used time and time again:
.translationTask { session in
self.viewModel.myTranslationSession = session
}
Overall, it’s rather simple to produce rather accurate translations using the power of Translation. For more examples and documentation, Apple does provide a good demo and article that can be found here.
Message in a Bottle for UIKit Developers
Translation is a really cool feature, but not necessarily the most sought after. Chances are you may not have a use case for it at all. However, there are important messages for all developers (though particularly the UIKit ones) that are blatant in Translations interface.
The most obvious message is that, by making Translation only accessible through SwiftUI, Apple is forcing UIKit developers to have to work with SwiftUI. Even if they’re working in a UIKit based app, they’ll need to interact with SwiftUI through a UIHostingController. Knowing there are devs that are still not on board with SwiftUI, this is sure to upset many of them. Apple is willing to take that risk.
That’s because this move also demonstrates Apple’s commitment to and level of confidence in SwiftUI. SwiftUI is 5 years old now and has matured/grown significantly over the years. It was in WWDC23 that Apple directly stated that “if you haven’t begun to adopt SwiftUI into your projects, now is the time”. This show of commitment pairs with watchOS and visionOS going SwiftUI-only. There’s no doubt that UIKit and AppKit are not to be deprecated any time soon (or ever). But Apple has made it abundantly clear the tide will eventually turn towards SwiftUI, and they will make it so.
A Relationship Lesson in Concurrency and SwiftUI
Translation also offers a glimpse into how Apple wants developers to view the relationship between concurrency and SwiftUI. When we obtain a TranslationSession through the programmatic route, it’s done so through a translationTask and offers function calls that are all async.
Obtaining a session through this route ensures that any translations in progress are tied to the lifecycle of the View it is servicing. Remember that the .task modifier is a more recent addition to the SwiftUI lifecycle, residing between a View’s initializer and .onAppear, but also comes with a strong attachment to the lifecycle of the View. As the View lives and dies, so do any attached tasks.
This is a crucial enforcement of task management in a way that simplifies by association. Translation will only operate under the safety of a View lifecycle, even if that View is nested in a UIHostingController. And this is true for the Translation presentation, as well: it only presents within the lifecycle of the View it is anchored to.
Note: I understand that there’s an assumption being made here that translationTask is routing through the task modifier here, but the Captain’s instincts feel 100% certain we can roll with the tide on this one. Look how simple it is to implement:
struct MyCustomTask: ViewModifier {
func body(content: Content) -> some View {
content
.task {
// insert your async code here
}
}
}
extension View {
func myCustomTask() -> some View {
modifier(MyCustomTask())
}
}
Marrying concurrent functionality (especially in the Swift 6 era) to the SwiftUI lifecycle in this manner is a radical concept that developers could consider in their SwiftUI strategy. It’s a bond rooted in a trustworthy promise.
Conclusion: A Huge Point for SwiftUI
SwiftUI has been showing how much it wants to play well with others. Consider its relationship with Swift Data: custom property wrappers that streamlines data operations and the utilization of Environment as the plumbing for simple/reactive data access.
With concurrency being the large focus of Swift 6, it’s a no-brainer then that SwiftUI would be equipped with tools like the task modifier and Observation framework. Forcing new/updated first-party features to work solely through these channels are certainly passive aggressive moves to get developers to use SwiftUI… but they’re actually warranted and hold water.
Apple continues to demonstrate what this new world looks like, and its worth getting on board to explore.