Blog

Back to Blog

Flow by Flow

Posted on 27 Feb 2013 Written by Thomas Kelly

At the end of last month, ShinobiEssentials finished its beta period, and was released to the outside world, joining ShinobiCharts and ShinobiGrids as part of our ShinobiSuite package. The ShinobiEssentials bundle contains five different components to use in building your own iOS apps; sliding overlay, flow layout, accordion, tabbed view and progress & activity indicators. Out of all the ShinobiEssentials components, the flow layout has seen the most varied use cases and implementations. For those readers who haven’t experienced the flow layout, it provides a very quick way to display and rearrange multiple UIViews using common gestures. It makes it perfect for photo galleries and storyboards, but has also found its way into menus and the tabbed view component. Adding a view to it is simply done by calling the addManagedSubview: method, such as; 

...
UIView *newView = [[UIView alloc] initWithFrame:frame];
[flow addManagedSubview:newView];
...

One of the more subtle features of the flow layout control is the ability to drag a subview outside the layout, and even to place it inside another flow layout. The recent release of ShinobiPlay demonstrates this in the “Arrange” section of the Experience pages. Normally, flow layout would stop a view being dragged outside, but in “Arrange” we have 9 tasks which can be arranged across 3 flow layouts, representing “To Do” tasks, “In Progress” tasks and “Finished” tasks. This was created using a manager responsible for all flow layouts, and have it act as the delegate of each flow.

Play_Arrange

This post will guide you through how to create it yourself. I’ve used ARC to keep things simple, but you can easily do the same with your own memory management. You can download the source of the project, which may be useful when following the tutorial below. You’ll need a copy of the ShinobiEssentials Framework to use with this project. If you don’t have it already – download a trial version. As with all ShinobiEssentials components, you will also need to use the Quartzcore framework in your project.

First, create the Flow Manager as a UIView, conforming to the SEssentialsFlowLayoutDelegate protocol.

#import <ShinobiEssentials/SEssentialsFlowLayout.h>

@interface FlowManager : UIView <SEssentialsFlowLayoutDelegate>
-(void)addSubview:(UIView*)subview toFlowAtIndex:(int)index;
@end

In the initWithFrame: method, we can start setting up the flow layouts. We will also want to store each of them in an array. Two important settings we will need are “dragsOutsideBounds” set to YES, to let us drag a view past the edges of each flow layout, otherwise we would have trouble dragging it into another, and “clipsToBounds” set to NO, to make sure subviews are drawn in their entirety, even if parts of them spill over the edges of the flow layout.

@interface FlowManager ()
{
   NSMutableArray *flowLayouts;
}
@end 

@implementation FlowManager 

- (id)initWithFrame:(CGRect)frame
{
   self = [super initWithFrame:frame];
   if (self)
   {
       flowLayouts = [NSMutableArray new];
       for (int i = 0; i < 3; i++)
       {
           CGSize flowSize = CGSizeMake(frame.size.width/3, frame.size.height);
           CGRect flowFrame = CGRectMake(i*flowSize.width, 0, flowSize.width, flowSize.height);
           [self createFlowWithFrame:flowFrame];
       }
   }
   return self;
}

-(void)createFlowWithFrame:(CGRect)frame
{
   SEssentialsFlowLayout *flow = [[SEssentialsFlowLayout alloc] initWithFrame:frame];
   flow.flowDelegate = self;
   flow.dragsOutsideBounds = YES;
   flow.clipsToBounds = NO;
   [flowLayouts addObject:flow];
   [self addSubview:flow];
}
@end 

This sets up the flow layouts, but we want to add some content too, so add a method like so, and call it with any content you want added in;

@interface FlowManager : UIView <SEssentialsFlowLayoutDelegate>
-(void)addSubview:(UIView*)subview toFlowAtIndex:(int)index;
@end

...

@implementation FlowManager

... 

-(void)addSubview:(UIView*)subview toFlowAtIndex:(int)index
{
   SEssentialsFlowLayout *flow = [flowLayouts objectAtIndex:index];
   [flow addManagedSubview:subview];
}
@end

Once the flows are set up, we want to start moving items around. This is controlled with the flow delegate methods. We are mainly interested in 6 different methods;

  • didBeginEditInFlowLayout:
  • didEndEditInFlowLayout:
  • flowLayout:didDragView:
  • flowLayout:shouldMoveView:
  • flowLayout:didMoveView:
  • flowLayout:didNotMoveView:

The first two are called when a flow layout enters and exits, which we will use to make all flow layouts enter and exit edit mode simultaneously. “didDragView:” is called repeatedly when a view is moved during a drag, and will be where we write the heart of our logic, to change ownership of a view when it enters another flow layout. Finally, “shouldMoveView” is called before any drag is allowed, and will be used to block multiple items being dragged simultaneously, which can have some odd effects if not properly dealt with. “didMoveView” and “didNotMoveView” are called when a view is dropped, depending on whether or not the position of the view changed in the flow layout’s ordering.

Firstly, we will deal with beginning and ending edits;

- (void)didBeginEditInFlowLayout:(SEssentialsFlowLayout *)flow
{
   for (SEssentialsFlowLayout *otherFlow in flowLayouts)
   {
       if (flow != otherFlow)
       {
           [otherFlow beginEditMode];
       }
   }
} 

- (void)didEndEditInFlowLayout:(SEssentialsFlowLayout *)flow
{
   for (SEssentialsFlowLayout *otherFlow in flowLayouts)
   {
       if (flow != otherFlow)
       {
           [otherFlow endEditMode];
       }
   }
}

Simple enough! Next, for moving items between flows, we need to do some interesting calculations. The “didDragView:” method is called repeatedly whilst dragging a view. We want to check if it has entered the bounds of another flow layout, and transfer ownership if it has. Because each flow layout has a different frame of reference, we need to convert between them before we can check if the view has moved enough to change owner or not. We first convert everything to the manager’s frame of reference to do comparisons, then, if we have a match, we convert into the matching flow layout’s frame of reference and transfer the view.

-(void)flowLayout:(SEssentialsFlowLayout *)sourceFlow didDragView:(UIView *)view
{
   CGPoint dragPosition = [sourceFlow convertPoint:view.center toView:self];
   for (SEssentialsFlowLayout *destinationFlow in flowLayouts)
   {
       if (destinationFlow != sourceFlow)
       {
           if(CGRectContainsPoint(destinationFlow.frame, dragPosition))
           {
               //Convert the center to the new frame of reference
               view.center = [self convertPoint:dragPosition toView:destinationFlow]; 

               //Swap owners
               [sourceFlow unmanageSubview:view];
               [destinationFlow addManagedSubview:view];
           }
       }
   }
} 

With this, you can drag items between different flow layouts! Pretty cool, but there is one more step to watch out for. In a single flow, you can’t pick up two items at once. However with multiple flows, you can. Normally this would be fine, but what happens if you drag items from the left and right flows into the middle one? At this point, the flow layout would have trouble deciding which gesture corresponds to which view, and start going wrong. To stop this happening, we add small check, using the shouldMoveView: method, and reset the state in didMoveView: and didNotMoveView:

The method requires that we save state, so we know if we already have a gesture in progress, and block any subsequent gestures. In our private interface add an extra field to store the UIView currently in progress;

@interface FlowManager ()
{
   NSMutableArray *flowLayouts;
   UIView *viewInProgress;
}
@end
 
...
...
...

-(BOOL)flowLayout:(SEssentialsFlowLayout *)flow shouldMoveView:(UIView *)view
{
   if (!viewInProgress)
       viewInProgress = view;
 
   return view == viewInProgress;
}

-(void)flowLayout:(SEssentialsFlowLayout *)flow didMoveView:(UIView *)view
{
   viewInProgress = nil;
}
 
-(void)flowLayout:(SEssentialsFlowLayout *)flow didNotMoveView:(UIView *)view
{
   viewInProgress = nil;
} 

Multi Flow Example

Well there we have it; flow layouts which can talk to each other. We can drag items around, and change ownership between different flow layouts. If you haven’t already, you can download the source for the project to get you started on creating your own MultiFlow. The “Arrange” demo in ShinobiPlay uses this to create columns of “To Do”, “In Progress” and “Done”, with multiple tasks which can be dragged between columns, and we even have reports of a Chess App being built, using 64 flow layouts, one as each tile on the board! The ShinobiEssentials team would love to hear what you end up building, so do let us know!

Back to Blog