Thumbnail

Back to Blog

Customizing the Pull to Action

Posted on 28 Apr 2014 Written by Jan Akerman

The ShinobiSuite 2.6 release has brought a plethora of new features to our iOS Controls, including a brand new control in our ShinobiEssentials bundle – the Pull to Action.

The Pull to Action has become a very popular UI pattern and features heavily in the iOS UI as well as being present in a number of third party applications – Twitter and Facebook to name two. A typical use case for the control is to refresh data on some kind of feed or table view.

Twitter Pta

 An example of Twitter’s Pull to Action

 

The Pull to Action control is essentially a UIView that responds to a UIScrollView being pulled down past its content. As the scroll view is pulled into a negative offset the Pull to Action changes visually to indicate to the user how much further they need to pull to trigger the event. Once the event is triggered, the Pull to Action stays visible until the event is finished, finally retracting out of view. Of course, everything mentioned here is customizable – it’s a very flexible control.

In this blog post I’m going briefly explain how our Pull To Action component works, and show you how flexible it is to customization – creating a really cool effect. If you haven’t used Pull To Action before, then it would probably be helpful to take a look at the quick start guide before reading further, to get a brief overview of the control.

The sample project for this tutorial is available on github, or you can download the zip – it will probably help to follow the code as you read the tutorial.

The Pull to Action’s structure

The Pull to Action control has four important parts to it; the Pull to Action itself, its status view, its visualizer, and its delegate. By default, as the user pulls against the top of the scroll view the Pull to Action is resized by the visualizer to reveal its content to the user – this content you see is the status view. Once an action has been triggered the Pull to Action notifies the delegate and waits until the delegate responds that the action is finished. Don’t worry if this doesn’t fully make sense yet, the roles of these four components will become clear as we begin to customize our control.

Pull to Action

The Pull to Action control is itself a UIView, it contains the status view as a subview.

Status View

The status view is a UIView that confirms to the SEssentialsPullToActionStatusView protocol. This is the view that gives context to your users. If you take the Twitter’s implementation pictured above, it would be the view that provides the arrow and the text.

Visualizer

This is an object that conforms to the SEssentialsPullToActionVisualizer protocol and is responsible for manipulating the Pull to Action and its status view to create the desired visual transitions. It receives various messages from the Pull to Action control meaning you can respond to every Pull to Action event you’d ever need to! 

Delegate

In most of our controls the delegate is optional and provides the ability for you to customize / supplement the control with your own behaviour. However, the Pull to Action’s delegate is essential to its functionality. By default, the Pull to Action control will stay in view whilst the action is executing, giving the user feedback that the action is still underway. It will stay in this executing state until it is notified that the action has been completed.

Customizing the Pull to Action

By now you should feel like you have a little bit of understanding about how the various components of the Pull to Action come together. If you want to see some basic Pull to Action control examples then I suggest you grab yourself a trial and take a look the samples we include. The rest of this blog post will show you how to customize the Pull to Action control to create a completely different look and feel to our out of the box control. By the end of this post you should have something like this (looking much smoother on a real device than in this gif!):

Pull To Action

Firstly, we’ll set up the Pull to Action as usual. The first thing we need to do is set up a scroll view that will always bounce vertically to reveal our Pull to Action control. This ensures that our scroll view will allow our users to pull their scroll view downwards even when its content isn’t out of view. For us, this means that our users can always refresh the page no matter how much content there is on the scroll view.

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds];
    self.scrollView.alwaysBounceVertical = YES;
    self.scrollView.contentSize = self.scrollView.frame.size;
    [self.view addSubview:self.scrollView];
    …
}

Next we add some content onto the scroll view so we can see when our scroll view is actually scrolling. This will make it much easier for us to debug our control!

…
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0,
                                                           CGRectGetMidY(self.scrollView.bounds),
                                                           CGRectGetWidth(self.scrollView.bounds), 50)];
label.text = @"Some content..";
label.textAlignment = NSTextAlignmentCenter;
[self.scrollView addSubview:label];
…

Then we setup our pull to action component with our scroll view.

// Initialize our Pull to Action.
self.pullToAction = [[SEssentialsPullToAction alloc] initWithScrollView:self.scrollView];
self.pullToAction.delegate = self;
[self.scrollView addSubview:self.pullToAction];

Finally, add the following delegate method to our view controller to simulate some action being carried out. Note that we must call the actionCompleted selector to notify the Pull to Action that the triggered action is complete.

-(void)pullToActionTriggeredAction:(SEssentialsPullToAction *)pullToAction {
    [self.pullToAction performSelector:@selector(actionCompleted) withObject:nil afterDelay:3];
}

At this point, if you run your project you will see the Pull to Action control running in its out of the box state:

Pull To Action Default

Now let’s begin customizing!

To achieve the spinning view pictured in the earlier screenshot we want our Pull to Action control to be static on top of our scroll view. To do this we need to add pullToAction view to be a subview of our view controller’s view rather than our scroll view: we give it a frame and position in the centre of our view. 

self.pullToAction.frame = CGRectMake(0, 0, 50, 50);
self.pullToAction.center = CGPointMake(CGRectGetMidX(self.view.bounds), 200);
[self.view addSubview:self.pullToAction];

If you run the project at this point and drag your scroll view downwards you should see some visual artefacts. This is because the Pull to Action’s default visualizer is still manipulating the Pull to Action to create our out of the box effect. Let’s create a custom visualizer to use instead – we’ll create an empty visualizer implementation for now.

@interface CustomVisualizer : NSObject <SEssentialsPullToActionVisualizer>

@end

@implementation CustomVisualizer

-(void)pullToAction:(SEssentialsPullToAction *)pullToAction layoutStatusView:(UIView<SEssentialsPullToActionStatusView> *)statusView {
    // Placeholder.
}

-(void)pullToAction:(SEssentialsPullToAction *)pullToAction pulledAmountChanged:(CGFloat)pulledAmount {
    // Placeholder.
}

-(void)pullToActionActionCompleted:(SEssentialsPullToAction *)pullToAction {
    // Placeholder.
}

-(void)pullToActionTriggeredAction:(SEssentialsPullToAction *)pullToAction {    
    // Placeholder.
}

@end

Then add it to our Pull to Action control.

self.pullToAction.visualizer = [CustomVisualizer new];

The next step is to create our custom status view. We need to create a status view containing a single image view, which will contain our arrow image. So add an appropriate image to the project (we used this icon from VisualPharm), then edit your CustomStatusView:

@interface CustomStatusView : UIView <SEssentialsPullToActionStatusView>

@property UIImageView *icon;

@end

@implementation CustomStatusView

-(id)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    
    if (self) {
	_icon = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"Refresh"]];
        _icon.frame = self.bounds;
        [self addSubview:_icon];
    }
    return self;
}

@end

Next we need to set our custom status view on our Pull to Action control.

self.pullToAction.statusView = [[CustomStatusView alloc] initWithFrame:self.pullToAction.bounds];

Running the project again and you should see the arrow image in the middle of the screen. Now, we don’t want our Pull to Action component visible in the middle of the scroll view covering up the content when it isn’t being used so we’ll hide it away until the user begins interacting with it. First though, this brings us nicely on the Pull to Action’s concept of state.

State

As your users interact with the control it will move through five states. Understanding these states is key to being able to customize your Pull to Action control. Altogether, there are five states in total and they are listed below. We’ll be using three of these states to achieve the effect that we are aiming for.

I’ll run through the five states below:

  • Idle – The scroll view hasn’t begun moving towards its triggeredThreshold yet.
  • Pulling – Your user is currently dragging the scroll view downwards
  • Triggered – Your user has pulled the scroll view past the ‘triggered threshold’.
  • Executing – The action is currently being executed.
  • Retracting – Your user has ended their gesture and the scroll view is animating back to it’s idle state.

So, as I mentioned above, let’s hide the Pull to Action until the user begins pulling the scroll view downwards. We’ll need the control hidden initially so add this to your view controller’s viewDidLoad implementation.

    self.pullToAction.hidden = YES;

Now we have the Pull to Action hidden initially, let’s show it when the user is pulling and hide it when it returns to idle. It’s our visualizer’s job to handle transitions between states so we’ll handle it there. The protocol SEssentialsPullToActionVisualizer actually inherits from the protocol SEssentialsPullToActionDelegate. This means that the visualizer can receive all of the notifications that our delegate can, giving the visualizer the ability to customize our Pull to Action in many ways. In this case we need to hide our Pull to Action component when it is in the idle state and make it visible in every other state. So add the following method to your visualizer’s implementation.

-(void)pullToAction:(SEssentialsPullToAction *)pullToAction
 didChangeFromState:(SEssentialsPullToActionState)oldState
            toState:(SEssentialsPullToActionState)newState {
    switch (newState) {
        case SEssentialsPullToActionStateIdle:
            pullToAction.hidden = YES;
            break;
            
        default:
            pullToAction.hidden = NO;
            break;
    }
}

If you run your Pull to Action control and pull it downwards, you’ll see the arrows appear but you’ll notice that your control never actually disappears. This is because we are still missing a couple of key things in our visualizer!

As I mentioned previously, the visualizer is responsible for handling any animations or visual transitions between states. We might not want an action being triggered or our status view updating to a new state until our visualizer has had a chance to finish any transitions it might be doing. It’s for this reason that the Pull to Action control will not transition from its Triggered state to its Executing state or from its Executing state to its Idle state until it receives a message from the visualizer letting it know it’s finished.

Since we don’t have any animations between these states, we can simply tell our Pull to Action control to transition straight the way. To do this, add the following method implementations to your Pull to Action control.

-(void)pullToActionActionCompleted:(SEssentialsPullToAction *)pullToAction {
    [pullToAction resetToIdle];
}

-(void)pullToActionTriggeredAction:(SEssentialsPullToAction *)pullToAction {
    [pullToAction executeAction];
}

If you fire up your project now, pull it downwards and let go, you should see your Pull to Action component disappear after your action is completed:

Pull To Action No Spin

In our next step, we’re going to make the arrows rotate as our user pulls our control downwards.

As I mentioned above, one of our visualizer’s roles is to update the status view. The visualizer receives notifications when the pulled amount changes, so we need to forward this on to our status view’s updateForPulledAmountChanged:pullThreshold: method which is defined on our status view protocol.

-(void)pullToAction:(SEssentialsPullToAction *)pullToAction pulledAmountChanged:(CGFloat)pulledAmount {
    [pullToAction.statusView updateForPulledAmountChanged:pulledAmount pullThreshold:pullToAction.pullThreshold];
}

This method on the status view protocol gives us the opportunity to update our status view visually, showing the user how close they are to triggering their action. As the user drags the Pull to Action downwards we are going to increase the size of our arrows and rotate them, once around a full circle until we trigger our action. Add the following method implementation to your status view.

-(void)updateForPulledAmountChanged:(CGFloat)pulledAmount pullThreshold:(CGFloat)pullThreshold {
        CGFloat pullRatio = pulledAmount / pullThreshold;
        
        CGFloat rotationAngle = (M_PI * 2) * pullRatio;
        CGAffineTransform rotate = CGAffineTransformMakeRotation(rotationAngle);
        
        CGFloat scaleRatio = MIN (1, pullRatio);
        CGAffineTransform scale = CGAffineTransformMakeScale(scaleRatio, scaleRatio);

        _icon.transform = CGAffineTransformConcat(rotate, scale);
}

As you can see, we calculate how close we are to triggering our action, create CoreGraphics transformations, and apply them to the image view containing our arrows. If you load up your project now you should see your arrows rotate as you pull downwards.

The last thing we are going to do here is make our arrow spin whilst our action is executing. This will give our users some feedback that the action is still being carried out.

To do this we’ll need to add the methods startSpinning and stopSpinning to our status view’s interface. We’ll then recursively call a method that will cause our arrows to spin 360 degrees once the startSpinning method has been called. This will continue until we call our stopSpinning method. Add the following code to your status view’s interface.

@interface CustomStatusView : UIView <SEssentialsPullToActionStatusView>
…
-(void)startSpinning;
-(void)stopSpinning;
@end

We need to add an Boolean ivar to your status view’s implementation to keep track of whether we are still spinning.

@implementation CustomStatusView {
    BOOL _spin;
}

The start spinning method will set this Boolean to YES and call our spin360 method.

-(void)startSpinning {
    _spin = YES;

    [self spin360];
}

The spin360 method animates the arrows around in a full circle and will continue to do so until our Boolean ivar is set to NO.

-(void)spin360 {
    [UIView animateWithDuration:0.25 delay:0 options:UIViewAnimationOptionCurveLinear animations:^{
        self.icon.transform = CGAffineTransformRotate(self.icon.transform, M_PI_2);
}completion:^(BOOL finished){ if (finished) { if (_spin) { [self spin360]; } } }]; }

The stopSpinning method simply sets the ivar to NO, stopping the spinning.

-(void)stopSpinning {
    _spin = NO;
}

At this point, we’ve added the code to make our arrow spin. Let’s have our visualizer start the spinning when our action is triggered.

-(void)pullToActionTriggeredAction:(SEssentialsPullToAction *)pullToAction {
[pullToAction executeAction];
    
    CustomStatusView *statusView = (CustomStatusView *)pullToAction.statusView;
    [statusView startSpinning];
}

Then tell it to stop spinning when the action has completed.

-(void)pullToActionActionCompleted:(SEssentialsPullToAction *)pullToAction {
    CustomStatusView *statusView = (CustomStatusView *)pullToAction.statusView;
    [statusView stopSpinning];
    
    [pullToAction resetToIdle];
}

We’ve now got one last step. If the user drags our Pull to Action whilst it is executing it will interupt our spinning. We need to add a guard around the rotation and scale we apply in our status view’s updateForPulledAmountChanged: method to stop this whilst we are executing. Add the following guard arround your rotation code.

-(void)updateForPulledAmountChanged:(CGFloat)pulledAmount pullThreshold:(CGFloat)pullThreshold {
    if(!_spin) {
	// Our rotation code is here.	
    }
}

If you run your project now you should now have the customized Pull to Action we were aiming for. Pulling it downwards causes the arrows to scale and rotate. When we pass our pull threshold the Pull to Action starts to execute and the arrow begin rotating. When the action has completed its action the arrows will disappear:

Pull To Action

Hopefully this blog post has given you a good idea of the various components that build up the Pull to Action control, and has shown you some of the key concepts you need to understand whilst implementing your own custom Pull to Action control. 

Back to Blog