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

Max Seelemann
  • 9,344
  • 4
  • 34
  • 40

12 Answers12

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")
}
Eugene Tartakovsky
  • 1,594
  • 17
  • 22
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;
}
Trenton McKinney
  • 56,955
  • 33
  • 144
  • 158
Max Seelemann
  • 9,344
  • 4
  • 34
  • 40
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)
}
DTHENG
  • 188
  • 1
  • 7
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!");
    }
}
Stunner
  • 12,025
  • 12
  • 86
  • 145
amleszk
  • 6,192
  • 5
  • 38
  • 43
  • 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 */ } – zadr Sep 21 '13 at 23:08
  • 1
    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. – EricS Nov 07 '13 at 17:13
  • Thanks man! great sulotion! I suggest you to use the "[UIApplication sharedApplication].statusBarFrame.size.height" insteal of "20". ;) – Raphaël Pinto Jul 03 '14 at 15:59
  • 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 window – TimWhiting Jun 22 '15 at 16:59
  • @amleszk 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? – zumzum Oct 21 '15 at 14:44
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; 
}
JohnDugdale
  • 111
  • 1
  • 3
  • 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! – p0lAris Mar 19 '15 at 22:50
  • Doesn't work if there's a table view on the screen. Even with `tableView.scrollsToTop = false` – SoftDesigner Jan 18 '18 at 11:33
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.

manggit
  • 204
  • 2
  • 7
  • I tried a couple of these and this is the most reliable solution. – amleszk Oct 27 '12 at 03:18
  • 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. – amleszk Mar 23 '13 at 00:26
  • radar and apple bug report submitted. http://openradar.appspot.com/radar?id=2887402 – amleszk Mar 23 '13 at 00:52
  • 1
    This hacks seem risky, they break without warning. – mskw Apr 10 '13 at 21:28
  • 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"?) – ArtOfWarfare Apr 12 '13 at 23:50
  • 1
    just an FYI: this will mess with the status bar style in iOS8, as it seems to follow the top-most window. – Stefan Fisk Apr 29 '15 at 12:46
6

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
}
swordray
  • 833
  • 9
  • 21
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!");
}];
Aron Balog
  • 574
  • 6
  • 9
  • 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. – swift taylor Jun 07 '13 at 00:34
  • 8
    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. – chadbag Aug 19 '13 at 03:57
  • 1
    Thank you very much! This just solved my problem with a UIWebView containing a div spanning its whole height having overflow:scroll set. – mwidmann Sep 12 '13 at 15:31
  • 2
    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. – Lily Ballard May 01 '14 at 22:48
  • 1
    I used it in my app. App Store review went fine, no questions asked. – Aron Balog Jul 25 '14 at 03:22
  • @AronBalog Any solution in iOS 13 with Swift 5.0? – Jigar Tarsariya May 21 '20 at 08:06
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.

apb
  • 3,270
  • 3
  • 29
  • 23
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...

Daniel
  • 22,363
  • 9
  • 64
  • 71
  • 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? – Max Seelemann Sep 20 '10 at 17:41
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
jcesarmobile
  • 51,328
  • 11
  • 132
  • 176
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
        }
    }
}
f3dm76
  • 680
  • 7
  • 21