Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Drag separator to resize UIViews

What would be to the best way of implementing an interface which consists of UIViews which are separated by a line, and the line can resize the views?

In it's simplest form, it could look like this:

----------------
|              |  
| View A       |
|              |
|--------------|  < line which can be moved up and down, resizing the views
|              |  
| View B       |   
|              |  
---------------- 

It could have many more views.

My first thought would be making the line a draggable UIView with something like Touches, which resized the views according to it's position, but I'm sure there must be a more elegant solution.

like image 733
cannyboy Avatar asked Dec 16 '22 10:12

cannyboy


2 Answers

First, define a gesture that detects whether you started on a border, and if the gesture changes, moves said borders:

#import <UIKit/UIGestureRecognizerSubclass.h>

- (void)viewDidLoad
{
    [super viewDidLoad];

    // I use long press gesture recognizer so it's recognized immediately

    UILongPressGestureRecognizer *gesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)];
    gesture.minimumPressDuration = 0.0;
    gesture.allowableMovement = CGFLOAT_MAX;
    gesture.delegate = self;
    [self.containerView addGestureRecognizer:gesture];
}

- (void)handlePan:(UILongPressGestureRecognizer *)gesture
{
    static NSArray *matches;
    static CGPoint firstLocation;

    if (gesture.state == UIGestureRecognizerStateBegan)
    {
        firstLocation = [gesture locationInView:gesture.view];
        matches = [BorderBeingDragged findBordersBeingDraggedForView:gesture.view fromLocation:firstLocation];
        if (!matches)
        {
            gesture.state = UIGestureRecognizerStateFailed;
            return;
        }
    }
    else if (gesture.state == UIGestureRecognizerStateChanged)
    {
        CGPoint location    = [gesture locationInView:gesture.view];
        CGPoint translation = CGPointMake(location.x - firstLocation.x, location.y - firstLocation.y);
        [BorderBeingDragged dragBorders:matches translation:translation];
    }
}

// if your subviews are scrollviews, you might need to tell the gesture recognizer
// to allow simultaneous gestures

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
    return TRUE;
}

Second, define a BordersBeingDragged class that does the detection of borders and the changing of borders:

typedef enum NSInteger {
    kBorderTypeNone   = 0,
    kBorderTypeLeft   = 1 << 0,
    kBorderTypeRight  = 1 << 1,
    kBorderTypeTop    = 1 << 2,
    kBorderTypeBottom = 1 << 3
} BorderType;

@interface BorderBeingDragged : NSObject

@property (nonatomic, weak) UIView *view;
@property (nonatomic) BorderType borderTypes;
@property (nonatomic) CGRect originalFrame;

@end

static CGFloat const kTolerance = 15.0;

@implementation BorderBeingDragged

+ (NSArray *)findBordersBeingDraggedForView:(UIView *)view fromLocation:(CGPoint)point
{
    NSMutableArray *matches = nil;

    for (UIView *subview in view.subviews)
    {
        BorderType types = kBorderTypeNone;
        CGRect frame = subview.frame;

        // test top and bottom borders

        if (point.x >= (frame.origin.x - kTolerance) &&
            point.x <= (frame.origin.x + frame.size.width + kTolerance))
        {
            if (point.y >= (frame.origin.y - kTolerance) && point.y <= (frame.origin.y + kTolerance))
                types |= kBorderTypeTop;
            else if (point.y >= (frame.origin.y + frame.size.height - kTolerance) && point.y <= (frame.origin.y + frame.size.height + kTolerance))
                types |= kBorderTypeBottom;
        }

        // test left and right borders

        if (point.y >= (frame.origin.y - kTolerance) &&
            point.y <= (frame.origin.y + frame.size.height + kTolerance))
        {
            if (point.x >= (frame.origin.x - kTolerance) && point.x <= (frame.origin.x + kTolerance))
                types |= kBorderTypeLeft;
            else if (point.x >= (frame.origin.x + frame.size.width - kTolerance) && point.x <= (frame.origin.x + frame.size.width + kTolerance))
                types |= kBorderTypeRight;
        }

        // if we found any borders, add it to our array of matches

        if (types != kBorderTypeNone)
        {
            if (!matches)
                matches = [NSMutableArray array];

            BorderBeingDragged *object = [[BorderBeingDragged alloc] init];
            object.borderTypes   = types;
            object.view          = subview;
            object.originalFrame = frame;

            [matches addObject:object];
        }
    }

    return matches;
}

+ (void)dragBorders:(NSArray *)matches translation:(CGPoint)translation
{
    for (BorderBeingDragged *object in matches)
    {
        CGRect newFrame = object.originalFrame;

        if (object.borderTypes & kBorderTypeLeft)
        {
            newFrame.origin.x   += translation.x;
            newFrame.size.width -= translation.x;
        }
        else if (object.borderTypes & kBorderTypeRight)
        {
            newFrame.size.width += translation.x;
        }

        if (object.borderTypes & kBorderTypeTop)
        {
            newFrame.origin.y    += translation.y;
            newFrame.size.height -= translation.y;
        }
        else if (object.borderTypes & kBorderTypeBottom)
        {
            newFrame.size.height += translation.y;
        }

        object.view.frame = newFrame;
    }
}

@end
like image 125
Rob Avatar answered Dec 26 '22 12:12

Rob


You do essentially need to make the line-view draggable, but it doesn't need to be complicated.

  1. Put viewA and viewB into a containerView
  2. Add a pan gesture recognizer to the containerView configured for a single touch and set its delegate to your controller.
  3. Implement gestureRecognizerShouldBegin: from the UIGestureRecognizerDelegate protocol and only allow it to begin if the touch in in the vicinity of the line-view.
  4. In the gesture handler get the touch position in the containerView and set the line-view position and frames for viewA and viewB

That's pretty much it.

like image 35
jhabbott Avatar answered Dec 26 '22 12:12

jhabbott