imstillalive imstillalive - 3 months ago 10
iOS Question

iOS - Resize multiple views with touch-drag separators

How can I resize views with a separator? What I'm trying to do is something like Instagram layout app. I want to be able to resize views by dragging the line that separates the views.

I already looked into this question. It is similar to what I want to accomplish and I already tried the answers but it does not work if there are more than 2 views connected to a separator (if there are 3 or more view only 2 views resize when separator moves each time). I tried to change the code but I have no idea what to do or what the code means.

In my app I will have 2-6 views. The separator should resize all the views that is next to it.

Some examples of my views:

enter image description here

How can I accomplish this? Where do I start?

Rob Rob
Answer

There are lots of ways to accomplish this, but like Avinash, I'd suggest creating a "separator view" in between the various "content" UIView objects. Then you can drag that around. The trick here, though, is that you likely want the separator view to be bigger than just the narrow visible line, so that it will capture touches not only right on the separator line, but close to it, too.

Unlike that other answer you reference, nowadays I'd new recommend using autolayout so that all you need to do with the user gestures is update the location of the separator view (e.g. update the top constraint of the separator view), and then all of the other views will be automatically resized for you. I'd also suggest adding a low priority constraint on the size of the subviews, so that they're laid out nicely when you first set everything up and before you start dragging separators around, but that it will fail gracefully when the dragged separator dictates that the size of the neighboring views must change.

Finally, while we'd historically use gesture recognizers for stuff like this, with the advent of predicted touches in iOS 9, I'd suggest just implementing touchesBegan, touchesMoved, etc. Using predicted touches, you won't notice the difference on the simulator or older devices, but when you run this on a device capable of predicted touches (e.g. new devices like the iPad Pro and other new devices), you'll get a more responsive UX.

So a horizontal separator view class might look like the following.

static CGFloat const kTotalHeight = 44;                               // the total height of the separator (including parts that are not visible
static CGFloat const kVisibleHeight = 2;                              // the height of the visible portion of the separator
static CGFloat const kMargin = (kTotalHeight - kVisibleHeight) / 2.0; // the height of the non-visible portions of the separator (i.e. above and below the visible portion)
static CGFloat const kMinHeight = 10;                                 // the minimum height allowed for views above and below the separator

/** Horizontal separator view

 @note This renders a separator view, but the view is larger than the visible separator
 line that you see on the device so that it can receive touches when the user starts 
 touching very near the visible separator. You always want to allow some margin when
 trying to touch something very narrow, such as a separator line.
 */

@interface HorizontalSeparatorView : UIView

@property (nonatomic, strong) NSLayoutConstraint *topConstraint;      // the constraint that dictates the vertical position of the separator
@property (nonatomic, weak) UIView *firstView;                        // the view above the separator
@property (nonatomic, weak) UIView *secondView;                       // the view below the separator

// some properties used for handling the touches

@property (nonatomic) CGFloat oldY;                                   // the position of the separator before the gesture started
@property (nonatomic) CGPoint firstTouch;                             // the position where the drag gesture started

@end

@implementation HorizontalSeparatorView

#pragma mark - Configuration

/** Add a separator between views

 This creates the separator view; adds it to the view hierarchy; adds the constraint for height; 
 adds the constraints for leading/trailing with respect to its superview; and adds the constraints 
 the relation to the views above and below

 @param firstView  The UIView above the separator
 @param secondView The UIView below the separator
 @returns          The separator UIView
 */

+ (instancetype)addSeparatorBetweenView:(UIView *)firstView secondView:(UIView *)secondView {
    HorizontalSeparatorView *separator = [[self alloc] init];
    [firstView.superview addSubview:separator];
    separator.firstView = firstView;
    separator.secondView = secondView;

    [NSLayoutConstraint activateConstraints:@[
        [separator.heightAnchor constraintEqualToConstant:kTotalHeight],
        [separator.superview.leadingAnchor constraintEqualToAnchor:separator.leadingAnchor],
        [separator.superview.trailingAnchor constraintEqualToAnchor:separator.trailingAnchor],
        [firstView.bottomAnchor constraintEqualToAnchor:separator.topAnchor constant:kMargin],
        [secondView.topAnchor constraintEqualToAnchor:separator.bottomAnchor constant:-kMargin],
    ]];

    separator.topConstraint = [separator.topAnchor constraintEqualToAnchor:separator.superview.topAnchor constant:0]; // it doesn't matter what the constant is, because it hasn't been enabled

    return separator;
}

- (instancetype)init {
    self = [super init];
    if (self) {
        self.translatesAutoresizingMaskIntoConstraints = false;
        self.userInteractionEnabled = true;
        self.backgroundColor = [UIColor clearColor];
    }
    return self;
}

#pragma mark - Handle Touches

// When it first receives touches, save (a) where the view currently is; and (b) where the touch started

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.oldY = self.frame.origin.y;
    self.firstTouch = [[touches anyObject] locationInView:self.superview];
    self.topConstraint.constant = self.oldY;
    self.topConstraint.active = true;
}

// When user drags finger, figure out what the new top constraint should be

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    UITouch *touch = [touches anyObject];

    // for more responsive UX, use predicted touches, if possible

    if ([UIEvent instancesRespondToSelector:@selector(predictedTouchesForTouch:)]) {
        UITouch *predictedTouch = [[event predictedTouchesForTouch:touch] lastObject];
        if (predictedTouch) {
            [self updateTopConstraintOnBasisOfTouch:predictedTouch];
            return;
        }
    }

    // if no predicted touch found, just use the touch provided

    [self updateTopConstraintOnBasisOfTouch:touch];
}

// When touches are done, reset constraint on the basis of the final touch,
// (backing out any adjustment previously done with predicted touches, if any).

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self updateTopConstraintOnBasisOfTouch:[touches anyObject]];
}

/** Update top constraint of the separator view on the basis of a touch.

 This updates the top constraint of the horizontal separator (which moves the visible separator).
 Please note that this uses properties populated in touchesBegan, notably the `oldY` (where the
 separator was before the touches began) and `firstTouch` (where these touches began).

 @param touch    The touch that dictates to where the separator should be moved.
 */
- (void)updateTopConstraintOnBasisOfTouch:(UITouch *)touch {
    // calculate where separator should be moved to

    CGFloat y = self.oldY + [touch locationInView:self.superview].y - self.firstTouch.y;

    // make sure the views above and below are not too small

    y = MAX(y, self.firstView.frame.origin.y + kMinHeight - kMargin);
    y = MIN(y, self.secondView.frame.origin.y + self.secondView.frame.size.height - (kMargin + kMinHeight));

    // set constraint

    self.topConstraint.constant = y;
}

#pragma mark - Drawing

- (void)drawRect:(CGRect)rect {
    CGRect separatorRect = CGRectMake(0, kMargin, self.bounds.size.width, kVisibleHeight);
    UIBezierPath *path = [UIBezierPath bezierPathWithRect:separatorRect];
    [[UIColor blackColor] set];
    [path stroke];
    [path fill];
}

@end

A vertical separator would probably look very similar, but I'll leave that exercise for you.

Anyway, you could use it like so:

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    UIView *previousContentView = nil;

    for (NSInteger i = 0; i < 4; i++) {
        UIView *contentView = [self addRandomColoredView];
        [self.view.leadingAnchor constraintEqualToAnchor:contentView.leadingAnchor].active = true;
        [self.view.trailingAnchor constraintEqualToAnchor:contentView.trailingAnchor].active = true;
        if (previousContentView) {
            [HorizontalSeparatorView addSeparatorBetweenView:previousContentView secondView:contentView];
            NSLayoutConstraint *height = [contentView.heightAnchor constraintEqualToAnchor:previousContentView.heightAnchor];
            height.priority = 250;
            height.active = true;
        } else {
            [self.view.topAnchor constraintEqualToAnchor:contentView.topAnchor].active = true;
        }
        previousContentView = contentView;
    }
    [self.view.bottomAnchor constraintEqualToAnchor:previousContentView.bottomAnchor].active = true;
}

- (UIView *)addRandomColoredView {
    UIView *someView = [[UIView alloc] init];
    someView.translatesAutoresizingMaskIntoConstraints = false;
    someView.backgroundColor = [UIColor colorWithRed:arc4random_uniform(256)/255.0 green:arc4random_uniform(256)/255.0 blue:arc4random_uniform(256)/255.0 alpha:1.0];
    [self.view addSubview:someView];

    return someView;
}

@end

That yields something like:

enter image description here

As I mentioned, a vertical separator would look very similar. If you have complicated views with both vertical and horizontal separators, you'd probably want to have invisible container views to isolate the vertical and horizontal views. For example, consider one of your examples:

enter image description here

That would probably consist of two views that span the entire width of the device with a single horizontal separator, and then the top view would, itself, have two subviews with one vertical separator and the bottom view would have three subviews with two vertical separators.


There's a lot here, so before you try extrapolating the above example to handle (a) vertical separators; and then (b) the views-within-views pattern, make sure you really understand how the above example works. This isn't intended as a generalized solution, but rather just to illustrate a pattern you might adopt. But hopefully this illustrates the basic idea.