Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tapping a button in the StatusBar does not work on iOS device (iPhone Xr) #3589

Open
ThomasH99 opened this issue May 11, 2022 · 8 comments
Open
Assignees

Comments

@ThomasH99
Copy link
Contributor

Describe the bug
From Reddit post with same title.
I am working on implementing the standard iOS feature tap-to-scroll-to-top in my app. My approach is to replace the standard StatusBar (an empty container) with a button which I size to the same size as the StatusBar container (defined by the default CN1 theme).
However, it seems that the default height (defined by the "StatusBar" uiid in the default CN1 iOS theme) is too small and on the device (iPhone Xr), tapping the button doesn't work (although it works fine in the Simulator).
I'm guessing that either the default height of the StatusBar is too small (so maybe not reaching down into the 'tappable' area of the screen) or that some other issue prevents tapping the top of the screen (like the safeArea?).
I can post a test example code if the above description is not sufficient.

Shai feedback:
We already have that code: https://github.com/codenameone/CodenameOne/blob/master/CodenameOne/src/com/codename1/ui/Form.java#L598-L624
I'm guessing that the pull down in iOS native is grabbing the tap but I never got around to investigate this in xcode.

To Reproduce
Code I used to test on device (you can play around with the size of the statusbar and check whether the touch registers or not):

Form hi = new Form("Cannot tap statusbar on iOS device", BoxLayout.y());

Label statusBarHeight = new Label("0");
//buttons to increase/decrease statusbar height
Button increaseStatusBarHeight = new Button(Command.create("Increase 1px", null, ev -> {
    statusBarHeight.setText("" + (Integer.parseInt(statusBarHeight.getText()) + 1));
    getCurrentForm().revalidate();
}));
Button decreaseStatusBarHeight = new Button(Command.create("Decrease 1px", null, ev -> {
    statusBarHeight.setText("" + (Integer.parseInt(statusBarHeight.getText()) - 1));
    getCurrentForm().revalidate();
}));

//https://developer.apple.com/forums/thread/662466: Other iPhones: 44. 44 leaves a few pixels active at the bottom of the visible statusbar. 
Button px00 = new Button(Command.create("0px ", null, ev -> {
    statusBarHeight.setText("" + 0);
    getCurrentForm().revalidate();
}));
Button px32 = new Button(Command.create("32px ", null, ev -> {
    statusBarHeight.setText("" + 32);
    getCurrentForm().revalidate();
}));
Button px40 = new Button(Command.create("40px ", null, ev -> {
    statusBarHeight.setText("" + 40);
    getCurrentForm().revalidate();
}));
Button px44 = new Button(Command.create("44px ", null, ev -> {
    statusBarHeight.setText("" + 44);
    getCurrentForm().revalidate();
}));

Label countStatusBarTaps = new Label("0");
hi.add(FlowLayout.encloseIn(new Label("Count statusbar taps: "), countStatusBarTaps));

hi.add(increaseStatusBarHeight);
hi.add(decreaseStatusBarHeight);
hi.add(FlowLayout.encloseIn(new Label("Set statusbar height: "), px00, px40, px44));
hi.add(FlowLayout.encloseIn(new Label("Additional statusbar height: "), statusBarHeight));

Button statusBarButton = new Button() {
    @Override
    public void pointerReleased(int x, int y) {
        Log.p("StatusButton pressed");
        countStatusBarTaps.setText("" + (Integer.parseInt(countStatusBarTaps.getText()) + 1)); //count successful taps on statusbarButton
        super.pointerReleased(x, y);
    }

    @Override
    public Dimension getPreferredSize() {
        Container fakeStatusBar = new Container();
        fakeStatusBar.setUIID("StatusBar");
        Dimension statusBarDim = new Dimension(
                Display.getInstance().getDisplayWidth(), //force to full screen width
                fakeStatusBar.getPreferredH() + fakeStatusBar.getStyle().getVerticalMargins() //default statusbar height
                + Integer.parseInt(statusBarHeight.getText())); //forcing the statusbarbutton to filling up the statusbar area
        return statusBarDim;
    }
};
//Color the statusbar blue and titlebar grey to see their size.
statusBarButton.setShowEvenIfBlank(true);
statusBarButton.getAllStyles().setBgColor(0x84B1D0);
statusBarButton.getAllStyles().setBgTransparency(128);

Toolbar toolbar = hi.getToolbar();
toolbar.getTitleComponent().getAllStyles().setBgColor(0xcccccc);
toolbar.getTitleComponent().getAllStyles().setBgTransparency(128);

BorderLayout currentStatusBarLayout = (BorderLayout) toolbar.getLayout();
Container statusBarNorth = (Container) currentStatusBarLayout.getNorth();
statusBarNorth.setUIID("Container"); //remove margin/padding from the statusbar container
statusBarNorth.addComponent(statusBarButton); //add new statusBarButton 

hi.show();

Expected behavior
Taps on the device statusbar should be sent to the underlying components. This is happens correctly on the Simulator.

Smartphone (please complete the following information):

  • Device: iOS 15 (iPhone XR)
@shannah
Copy link
Collaborator

shannah commented Jun 12, 2022

I played around with this a little bit and found that iOS does not seem to deliver events to the app at all when the status bar is clicked. One workaround might be to hide the status bar. Right now this isn't possible because the prefersStatusBarHidden setting is hard-coded in the CodenameOne_GLViewController... it is toggled on and off when the keyboard is shown/hidden. I did test with this set to YES, and confirmed that it will deliver the taps in @ThomasH99's test case.

See [2] below also for some hacks to be notified of taps on the status bar. None of this looks like something I would want to put into our general tap handling flow. Perhaps we could add a build hint to hide the status bar, but I'm not sure that this is quite what you're looking for. To solve the problem at hand, it might take a native interface to do one of the hacks suggested in [2] below.

Ref:

  1. How to hide status bar in iOS: https://stackoverflow.com/questions/18979837/how-to-hide-ios-status-bar
  2. Some possible hacks to be notified when status bar is tapped: https://stackoverflow.com/questions/3753097/how-to-detect-touches-in-status-bar

@ThomasH99
Copy link
Contributor Author

Thanks a million @shannah for looking at this, with holidays I completely missed your feedback. Since iOS apps almost all scroll to the top of the screen when the status bar is tapped, I guess the event is delivered in some other way to the app. Would it be possible to map that call to a method on the main app class (similar to what is done for performBackgroundFetch())?

However, I guess this means you cannot add additional functionality to react on statusbar longpress and double-tap like I've done :-(

Otherwise, hiding the statusbar could always be useful for some apps, but I had the impression this was already possible in CN1 (can't remember how right now).

@ThomasH99
Copy link
Contributor Author

@shannah Sorry to be a bother, but following up on this again and hoping some solution exists :-). I've tried to find work-arounds at the app-level. But nothing really works well and feedback is that missing the tap-statusbar-to-scroll-to-top is quite annoying (just about every single iOS app implements this, so no good way to justify that a CN1 app doesn't).

Just to summarize, I would prefer to have access to the individual pointer events from the statusbar to enhance the functionality (like I already did and which works in the Simulator), but I can understand if this is a rare case, so simply being able to react to a tap on the statusbar would be better than nothing.

@shannah
Copy link
Collaborator

shannah commented Sep 25, 2022

This approach looks like the easiest to adapt. https://stackoverflow.com/a/39585227/2935174

override func viewDidLoad() {
    let scrollView = UIScrollView()
    scrollView.bounds = view.bounds
    scrollView.contentOffset.y = 1
    scrollView.contentSize.height = view.bounds.height + 1
    scrollView.delegate = self
    view.addSubview(scrollView)
}

func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool {
    debugPrint("status bar tapped")
    return false
}

The general idea would be:

  1. Create a native peer component for a UIScrollView.
  2. Pipe the scrollViewShouldScrollToTop() call back to an event handler. You could wrap this fairly easily in an API like addShouldScrollToTopListener(ActionListener), which would automatically create your scrollpane and add it to the form.

Reference cn1-native-controls for examples of native component wrappers: https://github.com/shannah/cn1-native-controls

If you don't want to have to write Objective-C directly, you could use the Cn1ObjC library to write it all in Java.
https://github.com/shannah/CN1ObjCBridge#creating-a-native-component

@ThomasH99
Copy link
Contributor Author

Thanks again @shannah ! I'm afraid this kind of development is out of my reach (or would require too much time to build the competences).

I guess I'm hoping that @shai-almog will take pity and find a way to make this work, it definitely seems worth having this in CN1 to enable this common gesture in CN1 apps.

And it seems to still an 'official feature' since https://www.codenameone.com/developer-guide.html says "statusBarScrollsUpBool: Indicates that a tap on the status bar should scroll up the UI, only relevant in OS’s where paintsTitleBarBool is true" - although it is unfortunately probably one of the bugs where it is easier to change the documentation than the code ;-)

I found the Apple link and add it for reference: https://developer.apple.com/documentation/uikit/uiscrollview/1619421-scrollstotop

@ThomasH99
Copy link
Contributor Author

Any chance?

@ThomasH99
Copy link
Contributor Author

Bumping it up again :-)

For me personally, I'd prefer simply to be able to place an (invisible) button over the status bar and then use it to do whatever I want, but intercepting the standard iOS scrollToTop would be better than nothing.

Reading the Apple doc (https://developer.apple.com/documentation/uikit/uiscrollview/1619421-scrollstotop?language=objc), I wondered if another solution could be to place an invisible / zero-height native "scroll view" near the top and use that to intercept the call?

I’ve tried to google (in case different search words could dig something useful up ;-)) and found these

https://stackoverflow.com/questions/59639974/how-to-detect-touch-on-status-bar-ios-13

Says to add this code:

override func touchesBegan(_ touches: Set, with event: UIEvent?) {
super.touchesBegan(touches, with: event)

let statusBarRect = UIApplication.shared.statusBarFrame
guard let touchPoint = event?.allTouches?.first?.location(in: self.window) else { return }

if statusBarRect.contains(touchPoint) {
  // tap on statusbar, do something
}

}

https://stackoverflow.com/questions/15937017/full-screen-uiwebview-without-status-bar-cannot-scroll-to-top

storybookjs/storybook#1227

https://stackoverflow.com/questions/30443177/touchesbegan-not-called-in-uiview-subclass
Says “enable userInteractionEnabled” - but this sounds pretty basic, so I assume that's something you've already done if needed.

@shai-almog
Copy link
Collaborator

What we currently have is a touch area at the top. In the simulator this works perfectly as you can tell. The problem is the iOS simulator/device.

Unfortunately, we don't use iOSs scrolling mechanism so this won't work for us.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants