Swift Safe Area Layout Guide and Visual Format Language

Viewed 11539

I want to use Apples visual format language to constrain a view to the new Safe Area Layout Guide in iOS 11. However, I get an exception:

-[NSLayoutYAxisAnchor nsli_superitem]: unrecognized selector sent to instance 0x1c447ed40

    //Make View Dictionary
    var views: [String: Any] = ["left": self.leftContainer]

    //Check swift version and add appropriate piece to the view dictionary
    if #available(iOS 11, *) {
        views["topGuide"] = self.view.safeAreaLayoutGuide.topAnchor
    }else{
        views["topGuide"] = self.topLayoutGuide
    }

    //Make the constraint using visual format language
    let leftVertical = NSLayoutConstraint.constraints(withVisualFormat: "V:[topGuide][left]|", options: [], metrics: nil, views: views)

    //Add the new constraint
    self.view.addConstraints(vertical)

The reason I like visual format language is because you a can add lot of constraints with less code in some cases.

Any Ideas?

4 Answers

I want to use Apples visual format language to constrain a view to the new Safe Area Layout Guide

You can't. There is no access to the safe area layout guide through the visual format language. I've filed a bug on this, and I suggest you do the same.

We've extended the visual formatting language here a bit, so now you can pin against "<|" when you mean safeAreaLayoutGuide. I wish Apple did something like that.

For example, if you have the following pre iOS 11 code:

[NSLayoutConstraint activateConstraints:[NSLayoutConstraint
     constraintsWithVisualFormat:@"V:[_button]-(normalPadding)-|"
    options:0 metrics:metrics views:views
]];

And now you want to make sure that the button sits above the safe bottom margin on iPhone X, then do this:

[NSLayoutConstraint activateConstraints:[NSLayoutConstraint
    mmm_constraintsWithVisualFormat:@"V:[_button]-(normalPadding)-<|"
    options:0 metrics:metrics views:views
]];

That's it. It'll anchor the button to the bottom of its superview on iOS 9 and 10, but anchor it to the bottom of its safeAreaLayoutGuide on iOS 11.

Please note that using "|>" to pin to the top won't exclude the status bar on iOS 9 and 10.

// In @interface/@implementation NSLayoutConstraint (MMMUtil)
// ...

+(NSArray<NSLayoutConstraint *> *)mmm_constraintsWithVisualFormat:(NSString *)format
    options:(NSLayoutFormatOptions)opts
    metrics:(NSDictionary<NSString *,id> *)metrics
    views:(NSDictionary<NSString *,id> *)views
{
    if ([format rangeOfString:@"<|"].location == NSNotFound && [format rangeOfString:@"|>"].location == NSNotFound ) {
        // No traces of our special symbol, so do nothing special.
        return [self constraintsWithVisualFormat:format options:opts metrics:metrics views:views];
    }

    if (![UIView instancesRespondToSelector:@selector(safeAreaLayoutGuide)]) {
        // Before iOS 11 simply use the edges of the corresponding superview.
        NSString *actualFormat = [format stringByReplacingOccurrencesOfString:@"<|" withString:@"|"];
        actualFormat = [actualFormat stringByReplacingOccurrencesOfString:@"|>" withString:@"|"];
        return [NSLayoutConstraint constraintsWithVisualFormat:actualFormat options:opts metrics:metrics views:views];
    }

    //
    // OK, iOS 11+ time.
    // For simplicity we replace our special symbols with a reference to a stub view, feed the updated format string
    // to the system, and then replace every reference to our stub view with a corresponding reference to safeAreaLayoutGuide.
    //

    UIView *stub = [[UIView alloc] init];
    static NSString * const stubKey = @"__MMMLayoutStub";
    NSString *stubKeyRef = [NSString stringWithFormat:@"[%@]", stubKey];
    NSDictionary *extendedViews = [@{ stubKey : stub } mmm_extendedWithDictionary:views];

    NSString *actualFormat = [format stringByReplacingOccurrencesOfString:@"<|" withString:stubKeyRef];
    actualFormat = [actualFormat stringByReplacingOccurrencesOfString:@"|>" withString:stubKeyRef];

    NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:actualFormat options:opts metrics:metrics views:extendedViews];

    NSMutableArray *processedConstraints = [[NSMutableArray alloc] init];
    for (NSLayoutConstraint *c in constraints) {
        UIView *firstView = c.firstItem;
        UIView *secondView = c.secondItem;
        NSLayoutConstraint *processed;
        if (firstView == stub) {
            if (![secondView isKindOfClass:[UIView class]]) {
                NSAssert(NO, @"We only support UIView with <| and |> anchors, got %@", secondView.class);
                continue;
            }
            processed = [self
                constraintWithItem:secondView.superview.safeAreaLayoutGuide attribute:_MMMOppositeAttribute(c.firstAttribute)
                relatedBy:c.relation
                toItem:secondView attribute:c.secondAttribute
                multiplier:c.multiplier constant:c.constant
                priority:c.priority
                identifier:@"MMMSafeAreaFirstItemConstraint"
            ];
        } else if (secondView == stub && [firstView isKindOfClass:[UIView class]]) {
            if (![firstView isKindOfClass:[UIView class]]) {
                NSAssert(NO, @"We only support UIView with <| and |> anchors, got %@", secondView.class);
                continue;
            }
            processed = [self
                constraintWithItem:firstView attribute:c.firstAttribute
                relatedBy:c.relation
                toItem:firstView.superview.safeAreaLayoutGuide attribute:_MMMOppositeAttribute(c.secondAttribute)
                multiplier:c.multiplier constant:c.constant
                priority:c.priority
                identifier:@"MMMSafeAreaSecondItemConstraint"
            ];
        } else {
            processed = c;
        }
        [processedConstraints addObject:processed];
    }

    return processedConstraints;
}

+ (instancetype)constraintWithItem:(id)view1 attribute:(NSLayoutAttribute)attr1
    relatedBy:(NSLayoutRelation)relation
    toItem:(id)view2 attribute:(NSLayoutAttribute)attr2
    multiplier:(CGFloat)multiplier constant:(CGFloat)c
    priority:(UILayoutPriority)priority
    identifier:(NSString *)identifier
{
    NSLayoutConstraint *result = [NSLayoutConstraint constraintWithItem:view1 attribute:attr1 relatedBy:relation toItem:view2 attribute:attr2 multiplier:multiplier constant:c];
    result.priority = priority;
    result.identifier = identifier;
    return result;
}

// @end

static inline NSLayoutAttribute _MMMOppositeAttribute(NSLayoutAttribute a) {
    switch (a) {
        // TODO: support trailing/leading in the same way
        case NSLayoutAttributeLeft:
            return NSLayoutAttributeRight;
        case NSLayoutAttributeRight:
            return NSLayoutAttributeLeft;
        case NSLayoutAttributeTop:
            return NSLayoutAttributeBottom;
        case NSLayoutAttributeBottom:
            return NSLayoutAttributeTop;
        // These two are special cases, we see them when align all X or Y flags are used.
        case NSLayoutAttributeCenterY:
            return NSLayoutAttributeCenterY;
        case NSLayoutAttributeCenterX:
            return NSLayoutAttributeCenterX;
        // Nothing more.
        default:
            NSCAssert(NO, @"We don't expect other attributes here");
            return a;
    }
}

@interface NSDictionary (MMMUtil)
- (NSDictionary *)mmm_extendedWithDictionary:(NSDictionary *)d;    
@end

@implementation NSDictionary (MMMUtil)

- (NSDictionary *)mmm_extendedWithDictionary:(NSDictionary *)d {

    if (!d || [d count] == 0)
        return self;

    NSMutableDictionary *result = [[NSMutableDictionary alloc] initWithDictionary:self];
    [result addEntriesFromDictionary:d];
    return result;
}

@end

I know it's not VFL, but there is a factory class called NSLayoutAnchor that makes creating constraints a bit more clean and concise.

For example, I was able to pin the top anchor of a UILabel to the top anchor of the safe area with one compact line:

label.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor).isActive = true

Note that safeAreaLayoutGuide requires iOS 11. For older versions, replace self.view.safeAreaLayoutGuide.topAnchor by self.topLayoutGuide.bottomAnchor.

Again, I know it's not VFL, but this seems to be what we have for now.

While you can't currently create visual constraints relative to the safe area, you can include the safe area in your constraint. For example:

int safeInsetTop = self.view.safeAreaInsets.top;
int safeInsetBottom = self.view.safeAreaInsets.bottom;
NSString *verticalConstraints = [NSString stringWithFormat:@"V:|-%d-[myView]-%d-|", safeInsetTop, safeInsetBottom];
constraints = [NSLayoutConstraint constraintsWithVisualFormat:verticalConstraints options:0 metrics:nil views:viewsDictionary];

More verbose than ideal, but works, and is reasonably efficient.

Related