Masonry source code analysis

Keywords: Attribute iOS

Authors: Dai Pei
Address: http://daipei.me/2017/06/03/Masonry source code analysis/
Reprinted please indicate the source
My blog has moved, new blog address: daipei.me

AutoLayout is a good thing, but the official API is really not easy to use, Masonry was born to provide a concise interface for AutoLayout, our project layout is all using Masonry, it can be said that it is difficult to move away from it.

Masonry is very simple to use:

[self.aView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.equalTo(self.view);
        make.top.equalTo(self.view.mas_top).offset(100);
        make.width.height.mas_equalTo(200);
}];

Start with mas_makeConstraints

The most frequently used method in Masonry is mas_makeConstraints: This method is used when adding constraints for the first time. There are three ways to set constraints:

- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *make))block;
- (NSArray *)mas_updateConstraints:(void(^)(MASConstraintMaker *make))block;
- (NSArray *)mas_remakeConstraints:(void(^)(MASConstraintMaker *make))block;

From the method name, it is easy to see what the three methods do. The second method is used when updating constraints. The third method is used when re-adding constraints. That is to say, when previous constraints do not need to be completely re-set constraints. It is important to note that if you want to re-set constraints, you must use the third method. Continuous calls to the first method are easy to cause constraints. Bundle conflict, although the program may not necessarily crash.

These three methods return an array of newly added constraints, but I've never used this return value. If I don't look at the source code, I don't know that these methods have return values.

Let's look at the implementation of mas_makeConstraints:

- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
    self.translatesAutoresizingMaskIntoConstraints = NO;
    MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
    block(constraintMaker);
    return [constraintMaker install];
}

First, set the translates Autoresizing MaskInto Constraints property to NO

By default, the autoresizing mask on a view gives rise to constraints that fully determine

the view's position. This allows the auto layout system to track the frames of views whose

layout is controlled manually (through -setFrame:, for example).

When you elect to position the view using auto layout by adding your own constraints,

you must set this property to NO. IB will do this for you.

Ultimately, the system uses constraints to organize views, but if you set it to YES, it will convert attributes like Frame you set to constraints, but if you want to add constraints yourself, that is, if you want to use AutoLayout, you must set this attribute to NO. If you use AutoLayout of Interface Builder, this property is automatically set to NO.

The second step is to instantiate a MASConstraintMaker type maker with the current View, where self is the view that calls mas_makeConstraints:

The third step is to execute the code in the incoming block and pass the maker just instantiated into the block to configure the maker.

Note: There was a puzzle that when we use Masonry for layout, we always refer to self directly in the block. Why not generate circular references? After looking at the source code, we can see the reason. First, this block must strongly refer to self. Suppose we are in a VC layout (in most cases), this self is VC. Then the VC strongly refers to the View calling the Masonry interface, but this view does not refer to the block. In fact, this block is not referenced by any object, so this is the case. A block is released after execution. The block refers to self, but self does not refer directly or indirectly to block, so there is no problem of circular reference.

Finally, an install message is sent to the maker to add user-set constraints to the view.

MASConstraintMaker

First look at its initialization method:

- (id)initWithView:(MAS_VIEW *)view {
    self = [super init];
    if (!self) return nil;

    self.view = view;
    self.constraints = NSMutableArray.new;

    return self;
}

Here MAS_VIEW is a macro:

#if TARGET_OS_IPHONE || TARGET_OS_TV
    #define MAS_VIEW UIView
#elif TARGET_OS_MAC
    #define MAS_VIEW NSView
#endif

The intention to use macros here is obvious. Masonry hopes to support not only iOS, but also tvOS and macOS.

Maker keeps the reference of current view. Of course, the reference here is weak reference. Although strong reference does not cause circular reference, the weak reference here is actually reasonable, because if view does not exist, the maker does not need to exist. View should not add 1 to the reference count because of the reference of maker.

At the same time, maker instantiates a variable array constraints, which holds constraints to be added to the current view.

Let's see what happens when make.left is called:

- (MASConstraint *)left {
    return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];
}
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
    return [self constraint:nil addConstraintWithLayoutAttribute:layoutAttribute];
}
- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
    MASViewAttribute *viewAttribute = [[MASViewAttribute alloc] initWithView:self.view layoutAttribute:layoutAttribute];
    MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:viewAttribute];
    ...
    if (!constraint) {
        newConstraint.delegate = self;
        [self.constraints addObject:newConstraint];
    }
    return newConstraint;
}

The final call we see is - constraint: addConstraintWithLayoutAttribute: This method, I deleted the temporarily unrelated code, but the deleted code will be mentioned later.

Because the incoming constraints are nil, go directly to the if judgment, in which the agent of the newly generated constraints is set to maker and added to the self.constraints array.

Finally, the newly generated constraint is returned.

MASConstraint

In the previous section, make.left returns a MASConstraint object. Let's see how the sentence make.left.equalTo(self.view) is invoked.

// MASConstraint.h
- (MASConstraint * (^)(id attr))equalTo;

In MAS Constraint.h, there is such an interface. It took me half a day to figure out what function this is. It's a function with a return value of block. The return value of this block is MAS Constraint. It accepts an id parameter. We can see how it is called:.equalTo(self.view), which is really strange, because we know that the method in OC can't use a bit of grammar. Only attributes can be invoked, so in fact, equalTo can be understood as a block type attribute, so that this method is actually the getter method of the block.

The implementation of this method is as follows:

- (MASConstraint * (^)(id))equalTo {
    return ^id(id attribute) {
        return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
    };
}

Here we go directly to the - equalToWithRelation method, which is an abstract method, which implements MASViewConstraint and MAS Composite Constraint by two subclasses of MASConstraint.

Note: The abstract method here is implemented in an interesting way. Masonry defines a macro called MASMethodNotImplemented(), which throws an exception. If the abstract method is called incorrectly at run time, it will cause crash. OC does not support the abstract method, but it is worth learning to implement the abstract method in a unique way.

In this method, a relation ship parameter, such as NSLayoutRelationEqual, is passed in the code above, which will be used in the subsequent layout.

The parameters passed in by calling different methods are different, such as - greaterThanOrEqualTo, NSLayoutRelationGreaterThanOrEqual, and LesThanOrEqualTo, NSLayoutRelationLess ThanOrEqual.

MASViewConstraint

Let's first look at the relatively simple subclass of MASConstraint, and focus on how this subclass implements the equalToWithRelation method described above:

- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
    return ^id(id attribute, NSLayoutRelation relation) {
        if ([attribute isKindOfClass:NSArray.class]) {
            ...
        } else {
            NSAssert(!self.hasLayoutRelation || self.layoutRelation == relation && [attribute isKindOfClass:NSValue.class], @"Redefinition of constraint relation");
            self.layoutRelation = relation;
            self.secondViewAttribute = attribute;
            return self;
        }
    };
}

I temporarily omitted the first judgment, in the else branch, first asserting that the constraint was not redefined.

Then set the layoutRelation to mark the self.hasLayoutRelation above as YES in the setter method, where the relation ship was mentioned earlier.

Finally, setting the secondViewAttribute, you can see that seconds will naturally think of whether there is first, indeed there is. First is the attribute of the current view. In fact, this is not difficult to understand. One constraint is to describe the relationship between two views (except for size constraint). So the three most important attributes of this MASViewConstraint are: first ViewAttribute, secondViewAttribute, and so on. Layout Relation.

The setter method of secondViewAttribute has a lot of content:

- (void)setSecondViewAttribute:(id)secondViewAttribute {
    if ([secondViewAttribute isKindOfClass:NSValue.class]) {
        [self setLayoutConstantWithValue:secondViewAttribute];
    } else if ([secondViewAttribute isKindOfClass:MAS_VIEW.class]) {
        _secondViewAttribute = [[MASViewAttribute alloc] initWithView:secondViewAttribute layoutAttribute:self.firstViewAttribute.layoutAttribute];
    } else if ([secondViewAttribute isKindOfClass:MASViewAttribute.class]) {
        _secondViewAttribute = secondViewAttribute;
    } else {
        NSAssert(NO, @"attempting to add unsupported attribute: %@", secondViewAttribute);
    }
}

There are three types of secondViewAttribute: NSValue, MAS_VIEW and MASViewAttribute. I can give three examples to correspond to these three situations.

make.width.mas_equalTo(100);
make.left.equalTo(self.view);
make.left.equalTo(self.view.mas_left);

The second and third lines are equivalent, and you can see from setter's code why the second and third examples are equivalent, because when the type of secondViewAttribute passed in is MAS_VIEW, an object of MASViewAttribute is instantiated first, which is configured with the outlayAttribute of the incoming View and the first View, so when self.vie is passed in. W will use left consistently with the attribute s of the current view.

The third line of self.view.mas_left is directly a MASViewAttribute object, which can be assigned directly.

MASViewAttribute

MASViewAttribute holds three things: view of type MAS_VIEW, item of type id, layoutAttribute of type NSLayoutAttribute.

Its initialization method has two ways:

- (id)initWithView:(MAS_VIEW *)view layoutAttribute:(NSLayoutAttribute)layoutAttribute {
    self = [self initWithView:view item:view layoutAttribute:layoutAttribute];
    return self;
}

- (id)initWithView:(MAS_VIEW *)view item:(id)item layoutAttribute:(NSLayoutAttribute)layoutAttribute {
    self = [super init];
    if (!self) return nil;

    _view = view;
    _item = item;
    _layoutAttribute = layoutAttribute;

    return self;
}

The item in the second method is usually the same object as the first view. When using Masonry's VC-related interface, it refers to ID < UILayoutSupport>.

Finally, we use the MASSViewAttribute of two views to build constraint s and add them to the relevant views.

Install

When the maker is configured, it is the install step. Look directly at some of the source code in the install:

- (void)install {
    ...
    MAS_VIEW *firstLayoutItem = self.firstViewAttribute.item;
    NSLayoutAttribute firstLayoutAttribute = self.firstViewAttribute.layoutAttribute;
    MAS_VIEW *secondLayoutItem = self.secondViewAttribute.item;
    NSLayoutAttribute secondLayoutAttribute = self.secondViewAttribute.layoutAttribute;
    ...
    MASLayoutConstraint *layoutConstraint
        = [MASLayoutConstraint constraintWithItem:firstLayoutItem
                                        attribute:firstLayoutAttribute
                                        relatedBy:self.layoutRelation
                                           toItem:secondLayoutItem
                                        attribute:secondLayoutAttribute
                                       multiplier:self.layoutMultiplier
                                         constant:self.layoutConstant];
    ...
    MAS_VIEW *closestCommonSuperview = [self.firstViewAttribute.view mas_closestCommonSuperview:self.secondViewAttribute.view];
    self.installedView = closestCommonSuperview;
    ...
    [self.installedView addConstraint:layoutConstraint];
    self.layoutConstraint = layoutConstraint;
    [firstLayoutItem.mas_installedConstraints addObject:self];
}

The main logic is concerned here. First, a layoutConstrain object is generated from two viewAttribute s using the system API, then - mas_closestCommonSuperview: method is called to get the nearest parent view of the two views, and finally, the constraints just generated are added to the parent view.

Note:mas_closestCommonSuperview: The logic is to fix a view first, then go up to the parent view of another view, exit if the same view is found, and fail to find the parent view of the first view, and continue to go through the parent view of the second view until the whole view is found or traversed.

There are many judgments, which can be divided into many situations, and I'm going to talk about the most common one here.

Finally, the constraint is saved and added to the installed Constraints array of the first view.

So far, the entire constraint addition logic is complete.

MASCompositeConstraint

I omitted a part of the code when I mentioned the execution of make.left, and I'll make it up here.

In the initial usage example, there is a sentence make.width.height.mas_equalTo(200); this sentence will eventually enter the following method:

- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
    MASViewAttribute *viewAttribute = [[MASViewAttribute alloc] initWithView:self.view layoutAttribute:layoutAttribute];
    MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:viewAttribute];
    if ([constraint isKindOfClass:MASViewConstraint.class]) {
        //replace with composite constraint
        NSArray *children = @[constraint, newConstraint];
        MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children];
        compositeConstraint.delegate = self;
        [self constraint:constraint shouldBeReplacedWithConstraint:compositeConstraint];
        return compositeConstraint;
    }
    ...
    return newConstraint;
}

When we call make.width, we return a MASConstraint object, which also has left, right, top, bottom, width, height and so on. When we call. height to make.width, we generate a MASCompositeConstraint object, CompoeConstraint, which holds an array of MASConstraint-type objects and replaces the original constrConstraint with CompoeConstraint. Masonry uses MASCompositeConstraint to support the behavior of setting multiple constraint s simultaneously in a sentence.

Let's look at another case, make.top.left.bottom.right.equalTo(self.view); in this sentence, make.top.left returns a MASCompositeConstraint object, and when calling. box, it enters the - constraint of MASCompositeConstraint: addraintWithLayoutAttribute: method, which is implemented as follows:

- (MASConstraint *)constraint:(MASConstraint __unused *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
    id<MASConstraintDelegate> strongDelegate = self.delegate;
    MASConstraint *newConstraint = [strongDelegate constraint:self addConstraintWithLayoutAttribute:layoutAttribute];
    newConstraint.delegate = self;
    [self.childConstraints addObject:newConstraint];
    return newConstraint;
}

It first gets its own agent, which is actually maker. We can see from the code that generated the MASCompositeConstraint before. Then we call the maker's - constraint: addConstraint With LayoutAttribute: method. The function of this method is very simple at this moment, which is to generate a new Constraint:

- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
    MASViewAttribute *viewAttribute = [[MASViewAttribute alloc] initWithView:self.view layoutAttribute:layoutAttribute];
    MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:viewAttribute];
    ...
    return newConstraint;
}

The omitted code is the part that will not be executed in this case.

Then set the new Constraint agent to self and add it to the self. child Constraints array. When you install it later, you can send an install message for each constraint in the array.

summary

The first time I read the source code, I chose Masonry, because the amount of code is not very large, but it actually jumped into the pit. Masonry's source code is really hard to read. Various variables with similar names, nested block s and abstract methods make reading difficult. However, this does not affect the library's excellence, it provides such a simple interface, so slippery to use, a perfect interpretation of the sentence: leave complexity to oneself, leave simplicity to others.

chain syntax

Masonry provides concise chain grammar by using a large number of blocks. Most of the methods in the class MASConstraint return a block, and the return value of the block is MASConstraint. The returned MASConstraint object can call the method that returns the block, which makes the chain grammar work in this way.

Abstract method

By defining macros:

#define MASMethodNotImplemented() \
    @throw [NSException exceptionWithName:NSInternalInconsistencyException \
                                   reason:[NSString stringWithFormat:@"You must override %@ in a subclass.", NSStringFromSelector(_cmd)] \
                                 userInfo:nil]

It's really creative to implement Abstract methods.

Automatic Completion of Macro

Let's look at the following code:

#define mas_equalTo(...)                 equalTo(MASBoxValue((__VA_ARGS__)))
#define mas_greaterThanOrEqualTo(...)    greaterThanOrEqualTo(MASBoxValue((__VA_ARGS__)))
#define mas_lessThanOrEqualTo(...)       lessThanOrEqualTo(MASBoxValue((__VA_ARGS__)))

#define mas_offset(...)                  valueOffset(MASBoxValue((__VA_ARGS__)))

@interface MASConstraint (AutoboxingSupport)

/**
 *  Aliases to corresponding relation methods (for shorthand macros)
 *  Also needed to aid autocompletion
 */
- (MASConstraint * (^)(id attr))mas_equalTo;
- (MASConstraint * (^)(id attr))mas_greaterThanOrEqualTo;
- (MASConstraint * (^)(id attr))mas_lessThanOrEqualTo;

/**
 *  A dummy method to aid autocompletion
 */
- (MASConstraint * (^)(id offset))mas_offset;

@end

When we use the mas_equalTo() method, we actually use the macro above, but Masonry still provides the method, which is written clearly in the annotation to make the macro complete automatically.

No circular reference

The most annoying thing about using blocks is circular references. Masonry's use of blocks provides us with elegant ways of using them. It doesn't bring the drawbacks of circular references. It's really excellent.

Posted by mrtechguy on Tue, 25 Jun 2019 12:36:38 -0700