Spentak
Spentak

Reputation: 3359

How to have constraints auto resize and reposition keyboard keys for landscape

I'm having a hard time figuring out how to get my desired result with constraints. I could do this via programmatically but it seems like there should be a better way.

Basically I have a keyboard.

  1. The leftmost key (Q) and the rightmost key (P) should be 3 points from the left and right sides.

  2. The 8 keys in-between the Q and the P keys should be equally spaced between the Q and P keys

  3. The keys should stretch in width to fill up the entire width of space between the Q and P keys, while still maintaining equal space between each other.

Basically the keyboard should do what Apple's native keyboard does when it rotates in landscape.

Can this be done with constraints or do I need to do this programmatically without constraints?

Upvotes: 3

Views: 599

Answers (1)

Benjohn
Benjohn

Reputation: 13887

On Layout

Yes. Auto layout can do this.

Provided that you want the layout to remain the same for landscape and portrait orientations (I'm pretty sure that you do), then once your constraints are set up, everything should all just work. If you want them to change, that's possible, but I'm not going to address this as it doesn't seem required.

A Clarification

In your question, you say:

Can this be done with constraints or do I need to do this programmatically without constraints?

I'm not certain, but I want to check you understand that Auto Layout isn't an interface builder only thing. You can also very naturally use Auto Layout from code by manipulating NSLayoutConstraints and adding them to views. So, you have probably at least three options:

  • Use Auto Layout and set up the constraints in Interface Builder.
  • Use Auto Layout and set up the constraints in code.
  • Don't use Auto Layout and move your views about in layoutSubviews: or something like that.

Choosing IB or Code

You could set all of this up in IB. Each key will need two horizontal constraints and two vertical constraints, so you'll be managing at least 104 constraints for a 26 key keyboard. Plus digits. And symbols. Doing this by hand will be excruciating, and when you get it right, someone in your team (maybe even you) will decide the key spacing just has to be one pixel more.

A keyboard grid has so much regularity and so many constraints that it will be much easier to just use a bit of code to set the constraints up.

To be clear: you will still be using Auto Layout, but you will be creating the constraints with code instead of maintaining them by hand.

Setting up constraints in code

I'm assuming you want the keys to have equal sizes. You can impose this, along with equal (specified) spacing between them and a fixed margin at the left and right to the superview with a constraint like this:

// Substitute your view names here.
UIButton *q, *w, *e, *r, *t, *y, *u, *i, *o, *p;

NSDictionary *const metrics = @{@"margin": @(3), @"gap": @(4)};
NSDictionary *const views = NSDictionaryOfVariableBindings(q,w,e,r,t,y,u,i,o,p);
NSArray *const constraints =
  [NSLayoutConstraint
   constraintsWithVisualFormat:
   @"H:|-margin-[q]-gap-[w(q)]-gap-[e(q)]-gap-[r(q)]-gap-[t(q)]-gap-[y(q)]-gap-[u(q)]-gap-[i(q)]-gap-[o(q)]-gap-[p(q)]-margin-|"
   options: NSLayoutFormatDirectionLeadingToTrailing
   metrics: metrics
   views: views];

// Add constraints to your container view so that they do something.
UIView *const container = [self view];
[container addConstraints: constraints];

But I'd be tempted to build a helper method that would take an array of views and generate the constraints from that. It'd go something like this:

-(NSArray *) constraintsForEqualSizeViews: (NSArray *) keys
                      withSuperviewMargin: (CGFloat) margin
                                 andSpace: (CGFloat) space
{
  NSMutableArray *const constraints = [NSMutableArray new];
  
  // Note: the keys must be in a superview by now so that
  // a constraint can be set against its edge.
  UIView *const leftKey = [keys firstObject];
  [constraints addObject:
   [NSLayoutConstraint constraintWithItem: leftKey
                                attribute: NSLayoutAttributeLeft
                                relatedBy: NSLayoutRelationEqual
                                   toItem: [leftKey superview]
                                attribute: NSLayoutAttributeLeft
                               multiplier: 0
                                 constant: margin]];
  
  UIView *const rightKey = [keys firstObject];
  [constraints addObject:
   [NSLayoutConstraint constraintWithItem: rightKey
                                attribute: NSLayoutAttributeRight
                                relatedBy: NSLayoutRelationEqual
                                   toItem: [rightKey superview]
                                attribute: NSLayoutAttributeRight
                               multiplier: 0
                                 constant: margin]];
  
  UIView *const templateKeyForEqualSizes = leftKey;
  NSDictionary *const metrics = @{@"space": @(space)};
  for(NSUInteger i=1; i<[keys count]; ++i)
  {
    NSDictionary *const views = @{@"previousKey": keys[i-1],
                                  @"thisKey": keys[i],
                                  @"templateKey": templateKeyForEqualSizes};

    [constraints addObjectsFromArray:
     [NSLayoutConstraint constraintsWithVisualFormat: @"H:[previousKey]-space-[thisKey(templateKey)]"
                                             options: NSLayoutFormatDirectionLeftToRight
                                             metrics: metrics
                                               views: views]];
  }
  
  NSArray *const immutableArrayToReturn = [constraints copy];
  return immutableArrayToReturn;
}

The auto-layout language is described in Apple's documentation.

UIView Frames

You don't need to think about setting up the original frames for the keys' views – this is another reason to keep away from interface builder for describing the keys. You could create your views like this:

-(NSArray*) keyRowArray: (NSString*) keyNamesString
{
  const NSUInteger keyCount = [keyNamesString length];
  NSMutableArray *const keys = [NSMutableArray arrayWithCapacity: keyCount];
  
  for(NSUInteger i=0; i<keyCount; ++i)
  {
    NSString *const keyName = [keyNamesString substringWithRange:NSMakeRange(i, 1)];

    // Look! No frame needed!
    XXYourCustomKeyView *const key = [XXYourCustomKeyView new];

    // Do key setup – you probably want to add button actions, and such.
    [key setName: keyName];
    [keys addObject: keyName];
  }
  
  return [keys copy];
}

UIView ownership

I wouldn't manage all these keys in a UIViewController directly. I'd encapsulate them inside a XXYourCustomKeyboardView class that can take a keyboard description (something like an array of strings of the keyboard rows). Your controller can create this, pass in the configuration, set itself as a delegate and happy days.

Hybrid I.B. + Code Approach

It's also possible that you could use a hybrid approach where you set up some keys and constraints in IB:

  • The non character keys: tab, shift, etc. Their custom size and special behaviour.
  • The left and right key of each row.
  • The alternation that offsets the rows from a square grid.

You can then add the keys between those using code similar to the above.

Working from the constraint creation method I gave above, you might find it more flexible if you split it in to separate methods in a category on NSLayoutConstraint:

@interface NSLayoutConstraint (Keyboard)
  // This is what it constraint method currently creates in the for loop.
  +(NSArray*) constraintsForViews: (NSArray*) views imposingEqualSpace: (CGFloat) space;
  +(NSArray*) constraintsForViewsImposingEqualWidth: (NSArray*) views;
  
  // This will also be useful to align a whole row.
  +(NSArray*) constraintsForViewsImposingSameTopAndBottom: (NSArray*) views;
@end

Let me know in comments if any of this isn't clear. I'll add clarifications if necessary.

Upvotes: 9

Related Questions