Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Fixed the ability to scroll to the top on iOS 13 #16820

Merged
merged 11 commits into from
Mar 4, 2020

Conversation

gaaclarke
Copy link
Member

@gaaclarke gaaclarke commented Feb 26, 2020

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.

@gaaclarke gaaclarke force-pushed the ios13-fix-scroll-to-top branch from f6570a4 to eceab41 Compare February 26, 2020 22:27
@gaaclarke gaaclarke marked this pull request as ready for review February 26, 2020 23:37
@gaaclarke gaaclarke requested review from cbracken and xster February 26, 2020 23:38

- (BOOL)scrollViewShouldScrollToTop:(UIScrollView*)scrollView {
CGPoint statusBarPoint = CGPointZero;
sendFakeTouchEvent(_engine.get(), statusBarPoint, flutter::PointerData::Change::kDown);
Copy link
Contributor

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

Copy link
Member Author

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.

Copy link
Contributor

@Kavantix Kavantix Mar 4, 2020

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?

Copy link
Member

@xster xster left a 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;
Copy link
Member

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.

Copy link
Member Author

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>
Copy link
Member

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

Copy link
Member Author

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;
Copy link
Member

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

Copy link
Member Author

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.

Copy link
Member

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.

Copy link
Member Author

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];
Copy link
Member

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

Copy link
Member Author

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);
Copy link
Member

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).

Copy link
Member Author

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.

Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member

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;
Copy link
Member

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)?

Copy link
Member Author

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.

Copy link
Member Author

@gaaclarke gaaclarke left a 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;
Copy link
Member Author

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;
Copy link
Member Author

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>
Copy link
Member Author

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;
Copy link
Member Author

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];
Copy link
Member Author

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);
Copy link
Member Author

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.

@gaaclarke
Copy link
Member Author

@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.

Copy link
Member

@xster xster left a 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;
Copy link
Member

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.

@gaaclarke
Copy link
Member Author

@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

Copy link
Member

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.

Copy link
Member Author

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);
Copy link
Member

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"];
Copy link
Member

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?

Copy link
Member Author

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"];
Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Member

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

@xster
Copy link
Member

xster commented Mar 4, 2020

LGTM

cyanglaz pushed a commit to flutter/website that referenced this pull request Nov 22, 2022
`handleStatusBarTouches` is removed in flutter/engine#16820, this part of the documentation is obsolete. 

Fixes flutter/flutter#63051 (comment)
atsansone pushed a commit to flutter/website that referenced this pull request Nov 22, 2022
`handleStatusBarTouches` is removed in flutter/engine#16820, this part of the documentation is obsolete. 

Fixes flutter/flutter#63051 (comment)
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants