Swift Safe Area Layout Guide and Visual Format Language
Asked Answered
A

4

28

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?

Anglocatholic answered 28/9, 2017 at 22:41 Comment(0)
R
46

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.

Rodrigo answered 28/9, 2017 at 23:43 Comment(4)
Can you link your bug report?Anglocatholic
Radar number is 33865966. My description is: "There is no way to create, in the iOS visual format language, a constraint pinning a view to its superview's safe area. There needs to be one."Rodrigo
any updates on this, if this has been fixed? I can't find "33865966" in the Open Radar anymore.Accusatorial
@Accusatorial It was never in Open Radar. I suggest that we all just stop using the visual format language; it was always inadequate, it is becoming more inadequate over time, and evidently it will never be updated. Use anchor notation and stack views and be happy.Rodrigo
T
12

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
Throe answered 12/10, 2017 at 13:41 Comment(1)
This the a very smooth implementation! Should be the accepted answer!Xenocryst
R
10

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.

Ruberta answered 13/11, 2017 at 12:42 Comment(2)
Thanks for the tip! Note that requires iOS 11+Soaring
@Soaring Documentation says iOS9.0+ so IMO NSLayoutAnchor is the easiest way to use auto-layout programmatically developer.apple.com/documentation/uikit/nslayoutanchorTrapes
L
1

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.

Landfall answered 28/1, 2020 at 12:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.