Repository navigation
[iOS] Add Translate API support to Engine - #189878
Conversation
Specifies support for iOS 17.4+ translation API, adding TranslateViewController, platform plugin handling, delegate integration, and unit/edit-menu tests.
There was a problem hiding this comment.
Code Review
This pull request integrates the iOS 17.4+ translation API into the Flutter iOS platform plugin by introducing TranslateViewController and adding a 'translate' action to the text input edit menu. Feedback on these changes identifies potential runtime crashes if term or flutterViewController are nil, precision loss from using abs() instead of fabs() on CGFloat coordinates, and potential UIKit hierarchy issues when dismissing the hosting controller synchronously. Additionally, suggestions are provided to improve Swift encapsulation, formatting, and consistency in platform-check casing.
| CGRect ipadBounds = | ||
| CGRectMake(fmin(transformedFirstRect.origin.x, transformedLastRect.origin.x), | ||
| transformedFirstRect.origin.y, | ||
| abs(transformedLastRect.origin.x - transformedFirstRect.origin.x), |
There was a problem hiding this comment.
Using abs() on floating-point values (CGFloat) will cause an implicit conversion to int, truncating the fractional part. This can lead to precision loss and layout bugs for the popover bounds. Use fabs() instead.
| abs(transformedLastRect.origin.x - transformedFirstRect.origin.x), | |
| fabs(transformedLastRect.origin.x - transformedFirstRect.origin.x), |
| swiftUIViewController.didMove(toParent: self) | ||
| } | ||
|
|
||
| @objc func makeTranslateHostingController(termToTranslate: String) -> UIViewController { |
There was a problem hiding this comment.
Since makeTranslateHostingController is only called internally within TranslateViewController, it should be marked private and the @objc attribute should be removed to improve encapsulation and avoid unnecessary Objective-C runtime exposure.
| @objc func makeTranslateHostingController(termToTranslate: String) -> UIViewController { | |
| private func makeTranslateHostingController(termToTranslate: String) -> UIViewController { |
| contentView.onDismiss = { [weak self] in | ||
| guard let self = self else { return } | ||
| self.willMove(toParent: nil) | ||
| self.view.removeFromSuperview() | ||
| self.removeFromParent() | ||
| } |
There was a problem hiding this comment.
Modifying the UIKit view hierarchy (removing the hosting controller's view and parent-child relationship) synchronously inside the onDismiss closure (which is triggered by the SwiftUI binding's set block during a view update) can cause undefined behavior or crashes. It is safer to dispatch this to the main queue asynchronously.
| contentView.onDismiss = { [weak self] in | |
| guard let self = self else { return } | |
| self.willMove(toParent: nil) | |
| self.view.removeFromSuperview() | |
| self.removeFromParent() | |
| } | |
| contentView.onDismiss = { [weak self] in | |
| DispatchQueue.main.async { | |
| guard let self = self else { return } | |
| self.willMove(toParent: nil) | |
| self.view.removeFromSuperview() | |
| self.removeFromParent() | |
| } | |
| } |
| Color.clear | ||
| .frame(maxWidth: .infinity, maxHeight: .infinity) | ||
| .translationPresentation( | ||
| isPresented: Binding( get: { true }, set: { shown in if !shown { onDismiss?() } } ), |
There was a problem hiding this comment.
| suggestedMenu:suggestedMenu]; | ||
| } | ||
| } else if ([type isEqualToString:@"translate"]) { | ||
| if (@available(ios 17.4, *)) { |
hellohuanlin
left a comment
There was a problem hiding this comment.
I think i reviewed this in the previous PR before? So carrying over the LGTM.
| // firstRectForRange cannot be used here as its current implementation does | ||
| // not always return the full rect of the range. | ||
| CGRect firstRect = [(FlutterTextInputView*)_textInputPlugin.textInputView | ||
| caretRectForPosition:(FlutterTextPosition*)range.start]; |
There was a problem hiding this comment.
I probably worth extracting a local:
FlutterTextInputView* plugin = (FlutterTextInputView*)_textInputPlugin.textInputView;
// ... stuff ...| } | ||
|
|
||
| - (void)showTranslateViewControllerForTerm:(NSString*)term { | ||
| if (term.length == 0) { |
There was a problem hiding this comment.
We should wrap the content of this method in an if (@available(iOS 17.4, *)) which it looks like you've done elsewhere, otherwise we risk runtime crashes.
That kind of raises the question -- does the framework need to know whether this worked or not? This method doesn't have a return so on iOS < 17.4 you won't be able to tell. Depending on how this is implemented in the framework, you could also have guards in the plugin message handler return FlutterMethodNotImplemented... or some other signal. But probably depends a lot on how the framework wires things up.
| [mockPlugin handleMethodCall:methodCall result:result]; | ||
| [self waitForExpectationsWithTimeout:1 handler:nil]; | ||
| } | ||
| } |
There was a problem hiding this comment.
This covers the happy path. It might be worth adding a test for the unhappy path where iOS 17.4 isn't available.
| /// be imported directly into Objective-C. | ||
| @available(iOS 17.4, *) | ||
| @objc(FLTTranslateViewController) | ||
| class TranslateViewController: UIViewController { |
There was a problem hiding this comment.
Since the class is TranslateViewController, we should name the file TranslateViewController.swift.
| /// This is necessary because UIHostingController is a generic class and cannot | ||
| /// be imported directly into Objective-C. | ||
| @available(iOS 17.4, *) | ||
| @objc(FLTTranslateViewController) |
There was a problem hiding this comment.
This should be:
@objc(FlutterTranslateViewController)
for consistency with the other framework classes.
| } | ||
|
|
||
| @available(iOS 17.4, *) | ||
| fileprivate struct ContentView: View { |
There was a problem hiding this comment.
This seems like a pretty generic name. Maybe TranslateContentView or TranslatePresentationView?
| if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { | ||
| // On iPad, the translate screen is presented in a popover view, and requires a rect to use as | ||
| // bounds | ||
| FlutterTextInputPlugin* _textInputPlugin = [self.engine textInputPlugin]; |
There was a problem hiding this comment.
This shouldn't have a leading underscore since that's used for ivars.
| // firstRectForRange cannot be used here as its current implementation does | ||
| // not always return the full rect of the range. | ||
| CGRect firstRect = [(FlutterTextInputView*)_textInputPlugin.textInputView | ||
| caretRectForPosition:(FlutterTextPosition*)range.start]; |
There was a problem hiding this comment.
Here and a few other places: I could be wrong, but you shouldn't need a FlutterTextPosition*, the default UITextPosition* should be all that's necessary and doesn't ened a cast. UIKit APIs don't care about the underlying type; they use our own code to manipulate these.
| [flutterViewController addChildViewController:translateViewController]; | ||
| [flutterViewController.view addSubview:translateViewController.view]; | ||
| [translateViewController didMoveToParentViewController:flutterViewController]; | ||
| } |
There was a problem hiding this comment.
What if we extracted a helper (CGRect)popoverBoundsForCurrentSelection and have showShareViewController, showLookUpViewController, and showTranslateViewControllerForTerm all call it?
There was a problem hiding this comment.
Thinking about it, you probably don't need to dedupe out the other (existing) code in the other methods this PR if you want to keep it focused on translate, but you could create the helper and use it your new method in this patch and then go back to clean up the others in a followup patch.
|
Overall this looks good! Mostly just small nits. |
|
Golden file changes have been found for this pull request. Click here to view and triage (e.g. because this is an intentional change). If you are still iterating on this change and are not ready to resolve the images on the Flutter Gold dashboard, consider marking this PR as a draft pull request above. You will still be able to view image results on the dashboard, commenting will be silenced, and the check will not try to resolve itself until marked ready for review. For more guidance, visit Writing a golden file test for Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing. |
cbracken
left a comment
There was a problem hiding this comment.
This is looking really good. Mostly just nits and then one kind of open question around the edge-case where we fail to actually present the translation view.
|
autosubmit label was removed for flutter/flutter/189878, because The base commit of the PR is older than 7 days and can not be merged. Please merge the latest changes from the main into this branch and resubmit the PR. |
|
autosubmit label was removed for flutter/flutter/189878, because - The status or check suite Dashboard Checks has failed. Please fix the issues identified (or deflake) before re-applying this label. |
|
autosubmit label was removed for flutter/flutter/189878, because - The status or check suite Google testing has failed. Please fix the issues identified (or deflake) before re-applying this label.
|

Split out engine portion of #180021 b/c of cupertino/material freeze.
Pre-launch Checklist
///).