24
votes

Is there a way to get a notification, a callback or some other means to call a method whenever a UIView becomes visible for the user, i.e. when a UIScrollview is the superview of some UIViews, and the ViewController of such a UIView shall get notified when its view is now visible to the user?

I am aware of the possible, but not so elegant solution of checking to which position the ScrollView scrolled (via UIScrollViewDelegate-methods) and compute if either one of the subviews is visible...
But I'm looking for a more universal way of doing this.

4
The navigation of my app is based on scrolling a UIScrollView horizontally. I am also intercepting touches via a subclassed UIWindow. The ViewController of the currently visible view therefore needs to register itself as delegate to the subclassed UIWindow. And this is the reason I want to get notified if a view becomes visible. - arne_

4 Answers

9
votes

I've managed to solve the problem this way:

First, add a category for UIView with the following method:

// retrieve an array containing all super views

-(NSArray *)getAllSuperviews
{
    NSMutableArray *superviews = [[NSMutableArray alloc] init];

    if(self.superview == nil) return nil;

    [superviews addObject:self.superview];
    [superviews addObjectsFromArray:[self.superview getAllSuperviews]];

    return superviews;
}

Then, in your View, check if the window-property is set:

-(void)didMoveToWindow
{
    if(self.window != nil)
        [self observeSuperviewsOnOffsetChange];
    else
        [self removeAsSuperviewObserver];
}

If it is set, we'll observe the "contentOffset" of each superview on any change. If the window is nil, we'll stop observing. You can change the keyPath to any other property, maybe "frame" if there is no UIScrollView in your superviews:

-(void)observeSuperviewsOnOffsetChange
{
    NSArray *superviews = [self getAllSuperviews];
    for (UIView *superview in superviews)
    {
        if([superview respondsToSelector:@selector(contentOffset)])
            [superview addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew context:nil];
    }
}

-(void)removeAsSuperviewObserver
{
    NSArray *superviews = [self getAllSuperviews];
    for (UIView *superview in superviews)
    {
        @try
        {
            [superview removeObserver:self forKeyPath:@"contentOffset"];
        }
        @catch(id exception) { }
    }
}

Now implement the "observeValueForKeyPath"-method:

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if([keyPath isEqualToString:@"contentOffset"])
    {
        [self checkIfFrameIsVisible];
    }
}

Finally, check if the view's frame is visible inside the window's frame:

-(void)checkIfFrameIsVisible
{
    CGRect myFrameToWindow = [self.window convertRect:self.frame fromView:self];
    if(myFrameToWindow.size.width == 0 || myFrameToWindow.size.height == 0) return;
    if(CGRectContainsRect(self.window.frame, myFrameToWindow))
    {
        // We are visible, do stuff now
    }
}
7
votes

If your view is exhibiting behavior, it should be within a view controller. On a view controller, the viewDidAppear method will be called each time the view appears.

- (void)viewDidAppear:(BOOL)animated
1
votes

I don't think there's a universal way to do this for views. Sounds like you're stuck with scrollViewDidEndScrolling and other ScrollViewDelegate methods. But I'm not sure why you say it's elegant, they're quite straightforward.

0
votes

view's layer property should tell us if that view is visible or not

[view.layer visibleRect];

but this isnt working for me.

So work around could be to use UiScrollView contentOffset property to calculate if particular view is visible or not.