-
Notifications
You must be signed in to change notification settings - Fork 6k
Fixed the ability to scroll to the top on iOS 13 #16820
Conversation
uiscrollview to FlutterViewController.
f6570a4
to
eceab41
Compare
shell/platform/darwin/ios/framework/Source/FlutterViewController.mm
Outdated
Show resolved
Hide resolved
|
||
- (BOOL)scrollViewShouldScrollToTop:(UIScrollView*)scrollView { | ||
CGPoint statusBarPoint = CGPointZero; | ||
sendFakeTouchEvent(_engine.get(), statusBarPoint, flutter::PointerData::Change::kDown); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldnt it make more sense to send a message to flutter instead of sending fake touch events? @xster
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW I did it this way since thats how the code was previously working. I tried to change as little as possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I asked heaving in mind that flutter/flutter#42560 will be implemented somewhere in the future.
But it might make more sense to change the way the tap is handled on the dart side.
I believe currently it is done by the scaffold and should probably be moved to the routes.
@xster should I make a separate issue for this or add a comment to the one linked?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The approach is sensible and LGTM.
Here's the fun part: I think we're not far infra-wise to test this in testing/scenario_app/ios tests to create an xcuitest that taps the top of the screen and that checks for a platform message in the framework mock. We don't even need earlgreyflutter since we're not using real flutter.
@@ -23,6 +23,8 @@ | |||
#import "flutter/shell/platform/darwin/ios/framework/Source/platform_message_response_darwin.h" | |||
#import "flutter/shell/platform/darwin/ios/platform_view_ios.h" | |||
|
|||
constexpr int kMicrosecondsPerSecond = 1000 * 1000; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add comment for what this is supposed to be for.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a mathematical constant like pi. It has meaning outside of the context of usage. Documentation and design should be hierarchical, if you start documenting where things are used, you'll get cyclical documentation which is harder to maintain and thus ultimately less useful.
Added static declaration.
@@ -86,7 +88,7 @@ - (void)invalidate { | |||
// This is left a FlutterBinaryMessenger privately for now to give people a chance to notice the | |||
// change. Unfortunately unless you have Werror turned on, incompatible pointers as arguments are | |||
// just a warning. | |||
@interface FlutterViewController () <FlutterBinaryMessenger> | |||
@interface FlutterViewController () <FlutterBinaryMessenger, UIScrollViewDelegate> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a comment for the interface like binary messenger
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't document every time we implement a protocol in the codebase. FlutterBinaryMessenger was a special case since it was a special migration step.
@@ -123,6 +125,7 @@ @implementation FlutterViewController { | |||
// Coalescer that filters out superfluous keyboard notifications when the app | |||
// is being foregrounded. | |||
fml::scoped_nsobject<FlutterCoalescer> _updateViewportMetrics; | |||
fml::scoped_nsobject<UIScrollView> _scrollView; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a comment for why we own a uiscrollview
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All of that information is documented at the site of instantiation. We don't have a practice of documenting every instance variable, it would just be a duplication of what is said at the instantiation site.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that documentation is better served here than down below. If you see a _scrollView and don't know where it is, you can go-to-definition to see the reasoning. It's less direct to see where the scoped_nsobject is set and then find the local variable's construction which populates it.
You can also add one more line to say that UIScrollView specifically has a https://developer.apple.com/documentation/uikit/uiscrollviewdelegate/1619378-scrollviewshouldscrolltotop callback which we game here as a hint for future maintainers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
// status bar to trigger scrolling to the top of a scroll view. We place a UIScrollView offscreen | ||
// with a content offset so we can get those events. See also: | ||
// https://github.com/flutter/flutter/issues/35050 | ||
UIScrollView* scrollView = [[UIScrollView alloc] init]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comment for why in needs to be a scrollview rather than any uiview
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That information is already included in the current comment.
|
||
- (BOOL)scrollViewShouldScrollToTop:(UIScrollView*)scrollView { | ||
CGPoint statusBarPoint = CGPointZero; | ||
sendFakeTouchEvent(_engine.get(), statusBarPoint, flutter::PointerData::Change::kDown); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is a bit pre-emptive but it would be nice to check whether the flutter vc is top aligned before doing it (so we don't scroll if it's nested in the middle of the page).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is unnecessary because the statusbar is guaranteed to be top aligned if visible. If it isn't visible, the call to scrollViewShouldScrollToTop won't be called.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rather than being invisible, I meant if Flutter was embedded as a partial view in another parent native view. Would Flutter get this event even though it's not at the top of the screen? Maybe it already does the right thing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The scrollview doesn't need to be at the top of the screen, but it has to be on screen in order to get the event.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I meant rather that if Flutter was embedded as an entry in a native table, and you tap the top of the screen, you wouldn't want to send this synthesized touch to Flutter
@@ -23,6 +23,9 @@ | |||
#import "flutter/shell/platform/darwin/ios/framework/Source/platform_message_response_darwin.h" | |||
#import "flutter/shell/platform/darwin/ios/platform_view_ios.h" | |||
|
|||
constexpr int kMicrosecondsPerSecond = 1000 * 1000; | |||
constexpr CGFloat kScrollViewContentSize = 10.0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if 10x10 in a uiview equates a calayer with a buffer size of 10x10 (i.e. why not 1x1)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1x1 seems risky since technically a 1x1 shouldn't have an offset (unless fractional coordinates are allowed). I've moved it to 2x2, worse case scenario it represents 16bytes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@xster Let me try a bit with the test. Here is the problem though: In order to catch the breakage that happened in iOS 13 we'd have to run our tests against the latest OS as soon as it was launched. Right now there is a lag time upgrading tests to the new operating system because they are usually closely tied to an operating system.
@@ -23,6 +23,8 @@ | |||
#import "flutter/shell/platform/darwin/ios/framework/Source/platform_message_response_darwin.h" | |||
#import "flutter/shell/platform/darwin/ios/platform_view_ios.h" | |||
|
|||
constexpr int kMicrosecondsPerSecond = 1000 * 1000; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a mathematical constant like pi. It has meaning outside of the context of usage. Documentation and design should be hierarchical, if you start documenting where things are used, you'll get cyclical documentation which is harder to maintain and thus ultimately less useful.
Added static declaration.
@@ -23,6 +23,9 @@ | |||
#import "flutter/shell/platform/darwin/ios/framework/Source/platform_message_response_darwin.h" | |||
#import "flutter/shell/platform/darwin/ios/platform_view_ios.h" | |||
|
|||
constexpr int kMicrosecondsPerSecond = 1000 * 1000; | |||
constexpr CGFloat kScrollViewContentSize = 10.0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1x1 seems risky since technically a 1x1 shouldn't have an offset (unless fractional coordinates are allowed). I've moved it to 2x2, worse case scenario it represents 16bytes.
@@ -86,7 +88,7 @@ - (void)invalidate { | |||
// This is left a FlutterBinaryMessenger privately for now to give people a chance to notice the | |||
// change. Unfortunately unless you have Werror turned on, incompatible pointers as arguments are | |||
// just a warning. | |||
@interface FlutterViewController () <FlutterBinaryMessenger> | |||
@interface FlutterViewController () <FlutterBinaryMessenger, UIScrollViewDelegate> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't document every time we implement a protocol in the codebase. FlutterBinaryMessenger was a special case since it was a special migration step.
@@ -123,6 +125,7 @@ @implementation FlutterViewController { | |||
// Coalescer that filters out superfluous keyboard notifications when the app | |||
// is being foregrounded. | |||
fml::scoped_nsobject<FlutterCoalescer> _updateViewportMetrics; | |||
fml::scoped_nsobject<UIScrollView> _scrollView; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All of that information is documented at the site of instantiation. We don't have a practice of documenting every instance variable, it would just be a duplication of what is said at the instantiation site.
// status bar to trigger scrolling to the top of a scroll view. We place a UIScrollView offscreen | ||
// with a content offset so we can get those events. See also: | ||
// https://github.com/flutter/flutter/issues/35050 | ||
UIScrollView* scrollView = [[UIScrollView alloc] init]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That information is already included in the current comment.
|
||
- (BOOL)scrollViewShouldScrollToTop:(UIScrollView*)scrollView { | ||
CGPoint statusBarPoint = CGPointZero; | ||
sendFakeTouchEvent(_engine.get(), statusBarPoint, flutter::PointerData::Change::kDown); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is unnecessary because the statusbar is guaranteed to be top aligned if visible. If it isn't visible, the call to scrollViewShouldScrollToTop won't be called.
@xster I played around with scenario tests for the engine. Since we don't have access to the flutter framework we can't add tests as part of this PR. I'll look into adding one for the framework. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's what I meant by the scenario tests at testing/scenario_app/ios/Scenarios/ScenariosUITests. We don't actually need the Flutter framework (nor do we care about whether the Flutter framework correctly handles this signal downstream in this specific side of the test).
The mock framework is at testing/scenario_app/lib/src. You can use a platform channel to send the fake touch event back for assertions.
The primary objective at this point wouldn't be so much to catch it on all multiplexed versions of iOS, but at least to have a regression test if we ever break this in a refactor. For the versions, it's also a chicken and egg issue between having the test driving and the test environment to get both sides in :)
@@ -123,6 +125,7 @@ @implementation FlutterViewController { | |||
// Coalescer that filters out superfluous keyboard notifications when the app | |||
// is being foregrounded. | |||
fml::scoped_nsobject<FlutterCoalescer> _updateViewportMetrics; | |||
fml::scoped_nsobject<UIScrollView> _scrollView; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that documentation is better served here than down below. If you see a _scrollView and don't know where it is, you can go-to-definition to see the reasoning. It's less direct to see where the scoped_nsobject is set and then find the local variable's construction which populates it.
You can also add one more line to say that UIScrollView specifically has a https://developer.apple.com/documentation/uikit/uiscrollviewdelegate/1619378-scrollviewshouldscrolltotop callback which we game here as a hint for future maintainers.
@xster added UI test |
// touches on the status bar to trigger scrolling to the top of a scroll view. We place a | ||
// UIScrollView with height zero and a content offset so we can get those events. See also: | ||
// https://github.com/flutter/flutter/issues/35050 | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: whiteline before rather than after.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
|
||
- (BOOL)scrollViewShouldScrollToTop:(UIScrollView*)scrollView { | ||
CGPoint statusBarPoint = CGPointZero; | ||
sendFakeTouchEvent(_engine.get(), statusBarPoint, flutter::PointerData::Change::kDown); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rather than being invisible, I meant if Flutter was embedded as a partial view in another parent native view. Would Flutter get this event even though it's not at the top of the screen? Maybe it already does the right thing.
- (void)testTapStatusBar { | ||
if (@available(iOS 13, *)) { | ||
XCUIApplication* systemApp = | ||
[[XCUIApplication alloc] initWithBundleIdentifier:@"com.apple.springboard"]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😮 so they straight up moved the status bar out of the app and added some special hook to the uiscrollview to be able to catch it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The hook for uiscrollviews always existed (since like ios 2.0), they didn't break that one.
[[self.application.statusBars firstMatch] tap]; | ||
} | ||
|
||
XCUIElement* addTextField = self.application.textFields[@"PointerChange.add"]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh ha. Though can't you just put the channel handler you had in the AppDelegate in this test itself? Then you can just [self waitForExpectations] against its own listener like AppLifecycleTests?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can't do that. AppLifecycleTests are unit tests and run in the same process as the app. UI tests run as a separate process, I'd have to do IPC to communicate between the processes. There is a builtin mechanism to do IPC for UI elements though so that's what I'm using here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doh, you're right, this is a XCUITest
LGTM |
`handleStatusBarTouches` is removed in flutter/engine#16820, this part of the documentation is obsolete. Fixes flutter/flutter#63051 (comment)
`handleStatusBarTouches` is removed in flutter/engine#16820, this part of the documentation is obsolete. Fixes flutter/flutter#63051 (comment)
Introduced a UIScrollView to FlutterViewController as a workaround.
Relevant issue: flutter/flutter#35050
Tested manually on iOS 13.3, iOS 12.4, iOS 10.3.1.