Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,6 @@ FLUTTER_EXPORT

@property(strong, nonatomic) UIWindow* window;

/**
* Handle StatusBar touches.
*
* Call this from your AppDelegate's `touchesBegan:withEvent:` to have Flutter respond to StatusBar
* touches. For example, to enable scroll-to-top behavior. FlutterAppDelegate already calls it so
* you only need to manually call it if you aren't using a FlutterAppDelegate.
*/
+ (void)handleStatusBarTouches:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event;

@end

#endif // FLUTTER_FLUTTERDARTPROJECT_H_
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,6 @@ FLUTTER_EXPORT
nibName:(nullable NSString*)nibName
bundle:(nullable NSBundle*)nibBundle NS_DESIGNATED_INITIALIZER;

- (void)handleStatusBarTouches:(UIEvent*)event;

/**
* Registers a callback that will be invoked when the Flutter view has been rendered.
* The callback will be fired only once.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,6 @@ + (FlutterViewController*)rootFlutterViewController {
return nil;
}

+ (void)handleStatusBarTouches:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event {
[self.rootFlutterViewController handleStatusBarTouches:event];
}

- (void)touchesBegan:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event {
[super touchesBegan:touches withEvent:event];
[[self class] handleStatusBarTouches:touches withEvent:event];
}

// Do not remove, some clients may be calling these via `super`.
- (void)applicationDidEnterBackground:(UIApplication*)application {
}
Expand Down
85 changes: 47 additions & 38 deletions shell/platform/darwin/ios/framework/Source/FlutterViewController.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy Markdown
Contributor

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
Copy Markdown
Contributor 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.


NSNotificationName const FlutterSemanticsUpdateNotification = @"FlutterSemanticsUpdate";

NSNotificationName const FlutterViewControllerWillDealloc = @"FlutterViewControllerWillDealloc";
Expand Down Expand Up @@ -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
Copy Markdown
Contributor

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
Copy Markdown
Contributor 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.

@property(nonatomic, readwrite, getter=isDisplayingFlutterUI) BOOL displayingFlutterUI;
@end

Expand Down Expand Up @@ -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
Copy Markdown
Contributor

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
Copy Markdown
Contributor 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
Copy Markdown
Contributor

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
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

}

@synthesize displayingFlutterUI = _displayingFlutterUI;
Expand Down Expand Up @@ -329,6 +332,46 @@ - (void)loadView {
self.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;

[self installSplashScreenViewIfNecessary];
// This is a workaround to accomodate iOS 13 and higher. There isn't a way to get touches on the
// 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
Copy Markdown
Contributor

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
Copy Markdown
Contributor 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.

scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth;
// The color shouldn't matter since it is offscreen.
scrollView.backgroundColor = UIColor.whiteColor;
scrollView.delegate = self;
// This is an arbitrary small size.
scrollView.contentSize = CGSizeMake(10, 10);
// This is an arbitrary offset that is not CGPointZero.
scrollView.contentOffset = CGPointMake(10, 10);
// This is some aribrary place offscreen.
scrollView.bounds = CGRectMake(0, -10, 0, 10);
[self.view addSubview:scrollView];
_scrollView.reset(scrollView);
}

static void sendFakeTouchEvent(FlutterEngine* engine,
CGPoint location,
flutter::PointerData::Change change) {
const CGFloat scale = [UIScreen mainScreen].scale;
flutter::PointerData pointer_data;
pointer_data.Clear();
pointer_data.physical_x = location.x * scale;
pointer_data.physical_y = location.y * scale;
pointer_data.kind = flutter::PointerData::DeviceKind::kTouch;
pointer_data.time_stamp = [[NSDate date] timeIntervalSince1970] * kMicrosecondsPerSecond;
auto packet = std::make_unique<flutter::PointerDataPacket>(/*count=*/1);
pointer_data.change = change;
packet->SetPointerData(0, pointer_data);
[engine dispatchPointerDataPacket:std::move(packet)];
}

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

Copy link
Copy Markdown
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
Copy Markdown
Contributor 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.

@Kavantix Kavantix Mar 4, 2020

Copy link
Copy Markdown
Contributor

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
Copy Markdown
Contributor

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
Copy Markdown
Contributor 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
Copy Markdown
Contributor

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
Copy Markdown
Contributor 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
Copy Markdown
Contributor

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

sendFakeTouchEvent(_engine.get(), statusBarPoint, flutter::PointerData::Change::kUp);
return NO;
}

#pragma mark - Managing launch views
Expand Down Expand Up @@ -569,7 +612,6 @@ - (void)flushOngoingTouches {
flutter::PointerData pointer_data;
pointer_data.Clear();

constexpr int kMicrosecondsPerSecond = 1000 * 1000;
// Use current time.
pointer_data.time_stamp = [[NSDate date] timeIntervalSince1970] * kMicrosecondsPerSecond;

Expand Down Expand Up @@ -835,6 +877,9 @@ - (void)viewDidLayoutSubviews {
CGSize viewSize = self.view.bounds.size;
CGFloat scale = [UIScreen mainScreen].scale;

// Purposefully place this offscreen.
_scrollView.get().bounds = CGRectMake(0.0, -10.0, viewSize.width, 0);
Comment thread
gaaclarke marked this conversation as resolved.
Outdated

// First time since creation that the dimensions of its view is known.
bool firstViewBoundsUpdate = !_viewportMetrics.physical_width;
_viewportMetrics.device_pixel_ratio = scale;
Expand Down Expand Up @@ -1146,42 +1191,6 @@ - (NSString*)contrastMode {
}
}

#pragma mark - Status Bar touch event handling

// Standard iOS status bar height in points.
constexpr CGFloat kStandardStatusBarHeight = 20.0;

- (void)handleStatusBarTouches:(UIEvent*)event {
CGFloat standardStatusBarHeight = kStandardStatusBarHeight;
if (@available(iOS 11, *)) {
standardStatusBarHeight = self.view.safeAreaInsets.top;
}

// If the status bar is double-height, don't handle status bar taps. iOS
// should open the app associated with the status bar.
CGRect statusBarFrame = [UIApplication sharedApplication].statusBarFrame;
if (statusBarFrame.size.height != standardStatusBarHeight) {
return;
}

// If we detect a touch in the status bar, synthesize a fake touch begin/end.
for (UITouch* touch in event.allTouches) {
if (touch.phase == UITouchPhaseBegan && touch.tapCount > 0) {
CGPoint windowLoc = [touch locationInView:nil];
CGPoint screenLoc = [touch.window convertPoint:windowLoc toWindow:nil];
if (CGRectContainsPoint(statusBarFrame, screenLoc)) {
NSSet* statusbarTouches = [NSSet setWithObject:touch];

flutter::PointerData::Change change = flutter::PointerData::Change::kDown;
[self dispatchTouches:statusbarTouches pointerDataChangeOverride:&change];
change = flutter::PointerData::Change::kUp;
[self dispatchTouches:statusbarTouches pointerDataChangeOverride:&change];
return;
}
}
}
}

#pragma mark - Status bar style

- (UIStatusBarStyle)preferredStatusBarStyle {
Expand Down