iOS - Resize multiple views with touch-drag separators

前端 未结 5 2005
暗喜
暗喜 2021-01-01 04:20

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

5条回答
  •  小蘑菇
    小蘑菇 (楼主)
    2021-01-01 04:35

    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 *)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 *)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 *)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:

    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:

    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.

提交回复
热议问题