How to detect touches in status bar
Asked Answered
C

12

38

I have custom view in my application which can be scrolled by the user. This view, however, does not inherit from UIScrollView. Now I want the user to be able to scroll this view to the top, just as any other scrollable view allows. I figured that there is no direct way to do so.

Google turned up one solution: http://cocoawithlove.com/2009/05/intercepting-status-bar-touches-on.html This no longer works on iOS 4.x. That's a no-go.

I had the idea of creating a scrollview and keeping it around somewhere, just to catch it's notifications and then forward them to my control. This is not a nice way to solve my problem, so I am looking for "cleaner" solutions. I like the general approach of the aforementioned link to subclass UIApplication. But what API can give me reliable info?

Are there any thoughts, help, etc...?

Edit: Another thing I don't like about my current solution is that it only works as long as the current view does not have any scroll views. The scroll-to-top gesture works only if exactly one scroll view is around. As soon as the dummy is added (see my answer below for details) to a view with another scrollview, the gesture is completely disabled. Another reason to look for a better solution...

Cheeks answered 20/9, 2010 at 15:44 Comment(1)
I wonder why the status bar intercepts touches if it doesn't even do anything,Paleethnology
C
46

So this is my current solution, which works amazingly well. But please come with other ideas, as I don't really like it...

  • Add a scrollview somewhere in your view. Maybe hide it or place it below some other view etc.
  • Set its contentSize to be larger than the bounds
  • Set a non-zero contentOffset
  • In your controller implement a delegate of the scrollview like shown below.

By always returning NO, the scroll view never scrolls up and one gets a notification whenever the user hits the status bar. The problem is, however, that this does not work with a "real" content scroll view around. (see question)

- (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView
{
    // Do your action here
    return NO;
}
Cheeks answered 20/9, 2010 at 17:46 Comment(3)
Hi Max, is this applicable for websites too? please see: #21803734Pomeroy
That is Amazing! Works in iOS13!Adalbert
Works in iOS 14 too. Really simple and niceBarta
T
52

Finally, i've assembled the working solution from answers here. Thank you guys.

Declare notification name somewhere (e.g. AppDelegate.h):

static NSString * const kStatusBarTappedNotification = @"statusBarTappedNotification";

Add following lines to your AppDelegate.m:

#pragma mark - Status bar touch tracking
- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    [super touchesBegan:touches withEvent:event];
    CGPoint location = [[[event allTouches] anyObject] locationInView:[self window]];
    CGRect statusBarFrame = [UIApplication sharedApplication].statusBarFrame;
    if (CGRectContainsPoint(statusBarFrame, location)) {
        [self statusBarTouchedAction];
    }
}

- (void)statusBarTouchedAction {
    [[NSNotificationCenter defaultCenter] postNotificationName:kStatusBarTappedNotification
                                                        object:nil];
}

Observe notification in the needed controller (e.g. in viewWillAppear):

[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(statusBarTappedAction:)             
                                             name:kStatusBarTappedNotification
                                           object:nil];

Remove observer properly (e.g. in viewDidDisappear):

[[NSNotificationCenter defaultCenter] removeObserver:self name:kStatusBarTappedNotification object:nil];

Implement notification-handling callback:

- (void)statusBarTappedAction:(NSNotification*)notification {
    NSLog(@"StatusBar tapped");
    //handle StatusBar tap here.
}

Hope it will help.


Swift 3 update

Tested and works on iOS 9+.

Declare notification name somewhere:

let statusBarTappedNotification = Notification(name: Notification.Name(rawValue: "statusBarTappedNotification"))

Track status bar touches and post notification. Add following lines to your AppDelegate.swift:

override func touchesBegan(_ touches: Set<UITouch>, 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) {
        NotificationCenter.default.post(statusBarTappedNotification)
    }
}

Observe notification where necessary:

NotificationCenter.default.addObserver(forName: statusBarTappedNotification.name, object: .none, queue: .none) { _ in
    print("status bar tapped")
}
Trichloroethylene answered 23/9, 2013 at 7:2 Comment(8)
Be sure that your AppDelegate extends UIResponder so that -touchesBegan:withEvent: is called (older project may not).Boong
You might want to add touch.window.windowLevel == UIWindowLevelStatusBar otherwise it will detect taps on top of a UIAlertViewPlacket
@Vyacheslav why dont you try it and let us knowDrivein
Hello guys. Noticed that there is still an activity going on :). Updated my answer, added Swift 3 code snippet and tested that the method still works in iOS 9 and 10.Trichloroethylene
Is there a way to only detect taps instead of allTouches?Eliza
@SevenSystems, no, it doesn'tErosive
What is the solution for iOS 13?Baste
did u find any solution for ios 13?Sb
C
46

So this is my current solution, which works amazingly well. But please come with other ideas, as I don't really like it...

  • Add a scrollview somewhere in your view. Maybe hide it or place it below some other view etc.
  • Set its contentSize to be larger than the bounds
  • Set a non-zero contentOffset
  • In your controller implement a delegate of the scrollview like shown below.

By always returning NO, the scroll view never scrolls up and one gets a notification whenever the user hits the status bar. The problem is, however, that this does not work with a "real" content scroll view around. (see question)

- (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView
{
    // Do your action here
    return NO;
}
Cheeks answered 20/9, 2010 at 17:46 Comment(3)
Hi Max, is this applicable for websites too? please see: #21803734Pomeroy
That is Amazing! Works in iOS13!Adalbert
Works in iOS 14 too. Really simple and niceBarta
M
14

Adding this to your AppDelegate.swift will do what you want:

override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
    super.touchesBegan(touches, withEvent: event)
    let events = event!.allTouches()
    let touch = events!.first
    let location = touch!.locationInView(self.window)
    let statusBarFrame = UIApplication.sharedApplication().statusBarFrame
    if CGRectContainsPoint(statusBarFrame, location) {
        NSNotificationCenter.defaultCenter().postNotificationName("statusBarSelected", object: nil)
    }
}

Now you can subscribe to the event where ever you need:

NSNotificationCenter.defaultCenter().addObserverForName("statusBarSelected", object: nil, queue: nil) { event in

    // scroll to top of a table view
    self.tableView!.setContentOffset(CGPointZero, animated: true)
}
Mich answered 17/9, 2015 at 23:2 Comment(3)
Thanks! This is exactly what I wanted. And SWIFT is really neat. I hope more people vote this answer up!Loupe
I'm trying to update this to Swift 4, but I'm having some troubleDrynurse
Any solution for iOS 13?Baste
U
11

Thanks Max, your solution worked for me after spending ages looking.

For information :

dummyScrollView = [[UIScrollView alloc] init];
dummyScrollView.delegate = self;
[view addSubview:dummyScrollView];
[view sendSubviewToBack:dummyScrollView];   

then

  dummyScrollView.contentSize = CGSizeMake(view.frame.size.width, view.frame.size.height+200);
  // scroll it a bit, otherwise scrollViewShouldScrollToTop not called
  dummyScrollView.contentOffset = CGPointMake(0, 1);  

//delegate :
- (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView
{
  // DETECTED! - do what you need to
  NSLog(@"scrollViewShouldScrollToTop");
  return NO;
}

Note that I had a UIWebView also which I had to hack a bit with a solution I found somewhere :

- (void)webViewDidFinishLoad:(UIWebView *)wv
{  
  [super webViewDidFinishLoad:wv];  

  UIScrollView *scroller = (UIScrollView *)[[webView subviews] objectAtIndex:0];
  if ([scroller respondsToSelector:@selector(setScrollEnabled:)])
    scroller.scrollEnabled = NO; 
}
Understate answered 14/1, 2011 at 14:27 Comment(2)
I am afraid this doesn't seem to work with the setup that I have. I have a main UIView over which I have a scroll view that contains all different pages of the application. I have inserted the code in the main view as well as in the AppDelegate (for UIWindow) but it doesn't work!Corrody
Doesn't work if there's a table view on the screen. Even with tableView.scrollsToTop = falseHaddock
V
11

Found a much better solution which is iOS7 compatible here :http://ruiaureliano.tumblr.com/post/37260346960/uitableview-tap-status-bar-to-scroll-up

Add this method to your AppDelegate:

- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    [super touchesBegan:touches withEvent:event];
    CGPoint location = [[[event allTouches] anyObject] locationInView:[self window]];
    if (CGRectContainsPoint([UIApplication sharedApplication].statusBarFrame, location)) {
        NSLog(@"STATUS BAR TAPPED!");
    }
}
Varicose answered 21/9, 2013 at 22:57 Comment(5)
Your code will fail on cases where the status bar is double-height, or no longer 20px. Try something like this instead: if (CGRectContainsPoint([UIApplication sharedApplication].statusBarFrame, location)) { /* touched */ }Louvar
This fails if rotated, even upside down. Using 20 pixels is better than statusBarFrame, though, because statusBarFrame includes the "in-call" portion of the statusBar, which I think you probably want to ignore.Malacostracan
Thanks man! great sulotion! I suggest you to use the "[UIApplication sharedApplication].statusBarFrame.size.height" insteal of "20". ;)Misdemeanor
I disagree that using 20px is better than status bar frame. Using statusBarFrame then the notification wont be posted if the status bar is hidden. If you really are trying to detect touches IN the status bar, and not just at the top of the windowBaeza
@Varicose Why is the toucesBegan not called at all if the status bar is hidden? Shouldn't it at least be called but then the status bar frame would have height zero etc...? Why isn't it called at all?Newsmagazine
G
10

I implemented this by adding a clear UIView over the status bar and then intercepting the touch events

First in your Application delegate application:didFinishLaunchingWithOptions: add these 2 lines of code:

self.window.backgroundColor = [UIColor clearColor];
self.window.windowLevel = UIWindowLevelStatusBar+1.f;

Then in the view controller you wish to intercept status bar taps (or in the application delegate) add the following code

UIView* statusBarInterceptView = [[[UIView alloc] initWithFrame:[UIApplication sharedApplication].statusBarFrame] autorelease];
statusBarInterceptView.backgroundColor = [UIColor clearColor];
UITapGestureRecognizer* tapRecognizer = [[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(statusBarClicked)] autorelease];
[statusBarInterceptView addGestureRecognizer:tapRecognizer];
[[[UIApplication sharedApplication].delegate window] addSubview:statusBarInterceptView];

In the statusBarClicked selector, do what you need to do, in my case I posted a notification to the notification center so that other view controllers can respond to the status bar tap.

Gingivitis answered 11/9, 2012 at 22:6 Comment(6)
I tried a couple of these and this is the most reliable solution.Varicose
side effects!!! just noticed - Facebook share view controller (SLComposeViewController) does not show the keyboard if you mess with the UIWindowLevel. This solution is actually not as good as once thought.Varicose
radar and apple bug report submitted. openradar.appspot.com/radar?id=2887402Varicose
This hacks seem risky, they break without warning.Irretentive
One question about all "hacks" for detecting status bar taps - what happens if the status bar currently has a different action associated with it by the system (IE, because there's a phone call in the background, so it says "Tap to Return to Call"?)Shannan
just an FYI: this will mess with the status bar style in iOS8, as it seems to follow the top-most window.Transitive
U
7

Use an invisible UIScrollView. Tested at iOS 10 & Swift 3.

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
}
Uda answered 20/9, 2016 at 3:23 Comment(6)
This is an excellent, simple, and foolproof strategy. The scrollView doesn't even have to be the topmost view. I put mine behind my webView and sized it to fill the parent view and it works great.Kluge
This is awesomeKelter
Any solution for iOS 13?Baste
@Uda How can i add this in AppDelegate >Baste
@JigarTarsariya Add those functions to your view controllers.Uda
Tested in iOS 17! I found that the scrollView does not need to be full height but it does need to be full width. I put it behind the status bar area only: scrollView.frame = CGRect(origin: .zero, size: CGSize(width: view.bounds.width, height: view.safeAreaInsets.top))Ultranationalism
A
4

You can track status bar tap by using following code:

[[NSNotificationCenter defaultCenter] addObserverForName:@"_UIApplicationSystemGestureStateChangedNotification"
                                                  object:nil
                                                   queue:nil
                                              usingBlock:^(NSNotification *note) {
        NSLog(@"Status bar pressed!");
}];
Adverse answered 28/5, 2013 at 8:16 Comment(6)
This works for me, however the notification is not just for touchUpInside, it's for any state change. As such whatever action I call for here gets called twice, on touch and on release.Horner
Is there any chance of an app that observes "_UIApplicationSystemGestureStateChangedNotification" being rejected in the app store? Notification names with _ in the front look kind of private to me.Lugo
Thank you very much! This just solved my problem with a UIWebView containing a div spanning its whole height having overflow:scroll set.Bailiwick
This is relying on an internal implementation detail of the system. Not only is it equivalent to using private API, but it also may break with arbitrary system updates.Melaniemelanin
I used it in my app. App Store review went fine, no questions asked.Adverse
@AronBalog Any solution in iOS 13 with Swift 5.0?Baste
T
1

One way, might not be the best, could be to put a clear UIView on top of the status bar and intercept the touches of the UIView, might help you out if nothing else comes up...

Towns answered 20/9, 2010 at 17:19 Comment(1)
Yep, that's an idea. But that would be even hackier than adding a arbitrary scrollview somewhere and tracking it's behavior, wouldn't it?Cheeks
S
1

If you're just trying to have a UIScrollView scroll to the top when the status bar is tapped, it's worth noting that this is the default behavior IF your view controller has exactly one UIScrollView in its subviews that has scrollsToTop set to YES.

So I just had to go and find any other UIScrollView (or subclasses: UITableView, UICollectionView, and set scrollsToTop to be NO.

To be fair, I found this info in the post that was linked to in the original question, but it's also dismissed as no longer working so I skipped it and only found the relevant piece on a subsequent search.

Sottish answered 5/9, 2014 at 15:21 Comment(0)
E
1

For iOS 13 this has worked for me, Objective-C category of UIStatusBarManager

@implementation UIStatusBarManager (CAPHandleTapAction)
-(void)handleTapAction:(id)arg1 {
    // Your code here
}
@end
Erosive answered 24/1, 2020 at 13:46 Comment(8)
Can you tell me that how to use this code in Swift 5.0 with iOS 13?Baste
you can't, it's an objective-c category, categories are not available in swiftErosive
Okay, So is there any solution for iOS 13?Baste
Ohh, But my code is in Swift 5 so any option for it?Baste
you can mix objective-c with swiftErosive
So can you give me some more details on this like how i can achieve this with swift? and here CAPHandleTapAction what is the use of this?Baste
Let us continue this discussion in chat.Baste
Any solution in Swift for iOS 13??Canteen
R
1

SwiftUI style, no AppDelegate

import SwiftUI
import UIKit

public extension View {

    /// for this to work make sure all the other scrollViews have scrollsToTop = false
    func onStatusBarTap(onTap: @escaping () -> ()) -> some View {
        self.overlay {
            StatusBarTabDetector(onTap: onTap)
                .offset(y: UIScreen.main.bounds.height)
        }
    }
}

private struct StatusBarTabDetector: UIViewRepresentable {

    var onTap: () -> ()

    func makeUIView(context: Context) -> UIView {
        let fakeScrollView = UIScrollView()
        fakeScrollView.contentOffset = CGPoint(x: 0, y: 10)
        fakeScrollView.delegate = context.coordinator
        fakeScrollView.scrollsToTop = true
        fakeScrollView.contentSize = CGSize(width: 100, height: UIScreen.main.bounds.height * 2)
        return fakeScrollView
    }

    func updateUIView(_ uiView: UIView, context: Context) {}

    func makeCoordinator() -> Coordinator {
        Coordinator(onTap: onTap)
    }

    class Coordinator: NSObject, UIScrollViewDelegate {

        var onTap: () -> ()

        init(onTap: @escaping () -> ()) {
            self.onTap = onTap
        }

        func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool {
            onTap()
            return false
        }
    }
}
Recede answered 2/6, 2023 at 7:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.