chromium/ios/chrome/browser/shared/ui/util/named_guide.mm

// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/chrome/browser/shared/ui/util/named_guide.h"

#import "base/apple/foundation_util.h"
#import "base/check.h"
#import "ios/chrome/common/ui/util/constraints_ui_util.h"

namespace {
// The key path for whether NSLayoutConstraints are active.
NSString* const kActiveKeyPath = @"active";
}  // namespace

@interface NamedGuide ()

// The constraints used to connect the guide to `constrainedView` or
// `constrainedFrame`.
@property(nonatomic, strong) NSArray* constraints;
// A dummy view that is used to support `constrainedFrame`.
@property(nonatomic, strong) UIView* constrainedFrameView;

// Updates `constraints` to constrain the guide to `view`.
- (void)updateConstraintsWithView:(UIView*)view;

// Updates `constrainedFrameView` according to `constrainedFrame` and
// `autoresizingMask`.  This function will lazily instantiate the view if
// necessary and set up constraints so that this layout guide follows the view.
- (void)updateConstrainedFrameView;

// Checks whether the constraints have been deactivated, resetting them if
// necessary.
- (void)checkForInactiveConstraints;

@end

@implementation NamedGuide
@synthesize name = _name;
@synthesize constrainedView = _constrainedView;
@synthesize constrainedFrame = _constrainedFrame;
@synthesize autoresizingMask = _autoresizingMask;
@synthesize constraints = _constraints;
@synthesize constrainedFrameView = _constrainedFrameView;

- (instancetype)initWithName:(GuideName*)name {
  if ((self = [super init])) {
    _name = name;
    _constrainedFrame = CGRectNull;
  }
  return self;
}

- (void)dealloc {
  [self resetConstraints];
}

- (NSString*)description {
  return [NSString
      stringWithFormat:@"<%@: %p - %@, layoutFrame=%@, owningView=%@>",
                       NSStringFromClass([self class]), self, self.name,
                       NSStringFromCGRect(self.layoutFrame), self.owningView];
}

#pragma mark - Accessors

- (BOOL)isConstrained {
  [self checkForInactiveConstraints];
  return self.constraints.count > 0;
}

- (void)setConstrainedView:(UIView*)constrainedView {
  if (_constrainedView == constrainedView) {
    return;
  }

  // Reset the constrained frame to null if specifying a new constrained view.
  if (constrainedView) {
    self.constrainedFrame = CGRectNull;
  }

  _constrainedView = constrainedView;
  [self updateConstraintsWithView:_constrainedView];
}

- (void)setConstrainedFrame:(CGRect)constrainedFrame {
  if (CGRectEqualToRect(_constrainedFrame, constrainedFrame)) {
    return;
  }

  // Reset the constrained view to nil if specifying a new constrained frame.
  if (!CGRectIsNull(constrainedFrame)) {
    self.constrainedView = nil;
  }

  _constrainedFrame = constrainedFrame;
  [self updateConstrainedFrameView];
}

- (void)setAutoresizingMask:(UIViewAutoresizing)autoresizingMask {
  if (_autoresizingMask == autoresizingMask) {
    return;
  }
  _autoresizingMask = autoresizingMask;
  [self updateConstrainedFrameView];
}

- (void)setConstraints:(NSArray*)constraints {
  if (_constraints == constraints) {
    return;
  }
  if (_constraints.count) {
    for (NSLayoutConstraint* constraint in _constraints) {
      [constraint removeObserver:self forKeyPath:kActiveKeyPath];
    }
    [NSLayoutConstraint deactivateConstraints:_constraints];
  }
  _constraints = constraints;
  if (_constraints.count) {
    [NSLayoutConstraint activateConstraints:_constraints];
    for (NSLayoutConstraint* constraint in _constraints) {
      [constraint addObserver:self
                   forKeyPath:kActiveKeyPath
                      options:NSKeyValueObservingOptionNew
                      context:nullptr];
    }
  }
}

- (void)setConstrainedFrameView:(UIView*)constrainedFrameView {
  if (_constrainedFrameView == constrainedFrameView) {
    return;
  }

  if (_constrainedFrameView) {
    [_constrainedFrameView removeFromSuperview];
  }
  _constrainedFrameView = constrainedFrameView;

  // The constrained frame view is inserted at the bottom of the owning view's
  // hierarchy in an effort to minimize additional rendering costs.
  if (_constrainedFrameView) {
    [self.owningView insertSubview:_constrainedFrameView atIndex:0];
  }
}

#pragma mark - Public

+ (instancetype)guideWithName:(GuideName*)name view:(UIView*)view {
  while (view) {
    for (UILayoutGuide* guide in view.layoutGuides) {
      NamedGuide* namedGuide = base::apple::ObjCCast<NamedGuide>(guide);
      if ([namedGuide.name isEqualToString:name]) {
        return namedGuide;
      }
    }
    view = view.superview;
  }
  return nil;
}

- (void)resetConstraints {
  _constrainedView = nil;
  _constrainedFrame = CGRectNull;
  self.constraints = nil;
}

#pragma mark - NSKeyValueObserving

- (void)observeValueForKeyPath:(NSString*)key
                      ofObject:(id)object
                        change:(NSDictionary*)change
                       context:(void*)context {
  DCHECK([key isEqualToString:kActiveKeyPath]);
  DCHECK([self.constraints containsObject:object]);
  DCHECK(!base::apple::ObjCCastStrict<NSLayoutConstraint>(object).active);
  [self checkForInactiveConstraints];
}

#pragma mark - Private

- (void)updateConstraintsWithView:(UIView*)view {
  if (view) {
    self.constraints = @[
      [self.leadingAnchor constraintEqualToAnchor:view.leadingAnchor],
      [self.trailingAnchor constraintEqualToAnchor:view.trailingAnchor],
      [self.topAnchor constraintEqualToAnchor:view.topAnchor],
      [self.bottomAnchor constraintEqualToAnchor:view.bottomAnchor]
    ];
  } else {
    self.constraints = nil;
  }
}

- (void)updateConstrainedFrameView {
  // Remove the dummy view if `constrainedFrame` is null.
  if (CGRectIsNull(self.constrainedFrame)) {
    self.constrainedFrameView = nil;
    return;
  }

  // Lazily create the view if necessary and set it up using the specified frame
  // and autoresizing mask.
  // NOTE: The view's `translatesAutoresizingMaskIntoConstraints` remains set to
  // the default value of `YES` in order to leverage UIKit's built in frame =>
  // constraint conversion.
  if (!self.constrainedFrameView) {
    self.constrainedFrameView = [[UIView alloc] init];
    self.constrainedFrameView.backgroundColor = [UIColor clearColor];
    self.constrainedFrameView.userInteractionEnabled = NO;
  }
  self.constrainedFrameView.frame = self.constrainedFrame;
  self.constrainedFrameView.autoresizingMask = self.autoresizingMask;
  [self updateConstraintsWithView:self.constrainedFrameView];
}

- (void)checkForInactiveConstraints {
  for (NSLayoutConstraint* constraint in self.constraints) {
    if (constraint.active) {
      return;
    }
  }
  [self resetConstraints];
}

@end