IOS 自動佈局-UIStackPanel和UIGridPanel(二)

在上一篇中我提到了如何使用stackpanel和gridpanel來實現自動佈局。而在這一篇中我着重講解下其中的原理。iview

在(UIPanel   UIStackPanel  UIGridPanel)中主要是使用了NSLayoutConstraint這個類來實現的,所以爲了看懂下面的代碼請務必先了解NSLayoutConstraint的使用方法。佈局

先考慮下這樣一個場景,如今有一個自上而下垂直的佈局,水平方向的寬度跟屏幕分辨率的寬度保持一致,垂直方向高度不變,各個視圖間的間距不變,在用戶切換橫屏和豎屏的時候只有視圖的寬度是改變的,而高度和視圖間的間距不變。這樣一個場景也能模擬咱們的應用在不一樣分辨率上適配。ui

針對上面這個場景,那麼咱們勢必要給UIView兩個屬性,就是描述UIView高寬和UIView之間間距的屬性,這裏定義爲size和margin屬性,size的類型是CGSize,而margin的數據類型是UIEdgeInsets(描述該UIView的四個方向的間距)。這兩個屬性是以擴展屬性實現的。atom

代碼以下:spa

@interface UIView(UIPanelSubView)
//設置view的大小
@property (nonatomic,assign)CGSize size;
//view距離左上右下的間距
@property (nonatomic,assign)UIEdgeInsets margin;
@end

既然有了這兩個屬性,那麼意味着只要我修改了兩個屬性的任何一個屬性,都能實時的改變UIView的外觀,那麼咱們這裏就須要有一個方法來充值UIView的實現,這裏添加一個方法resetConstraints,用來重置約束。code

這樣完整的class定義是這樣的orm

@interface UIView(UIPanelSubView)
//設置view的大小
@property (nonatomic,assign)CGSize size;
//view距離左上右下的間距
@property (nonatomic,assign)UIEdgeInsets margin;
//重置約束
-(void)resetConstraints;
@end

完整的實現代碼以下:blog

@implementation UIView(UIPanelSubView)
char* const uiviewSize_str = "UIViewSize";
-(void)setSize:(CGSize)size{
    objc_setAssociatedObject(self, uiviewSize_str, NSStringFromCGSize(size), OBJC_ASSOCIATION_RETAIN);
    //先將原來的高寬約束刪除
    for(NSLayoutConstraint *l in self.constraints){
        switch (l.firstAttribute) {
            case NSLayoutAttributeHeight:
            case NSLayoutAttributeWidth:
                [self removeConstraint:l];
                break;
            default:
                break;
        }
    }
    //添加高度約束
    [self addConstraint:[NSLayoutConstraint constraintWithItem:self
                                                     attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0f constant:size.height]];
    //添加寬度約束
    [self addConstraint:[NSLayoutConstraint constraintWithItem:self
                                                     attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0f constant:size.width]];
}

-(CGSize)size{
    return CGSizeFromString(objc_getAssociatedObject(self, uiviewSize_str));
}

char* const uiviewMargin_str = "UIViewMargin";
-(void)setMargin:(UIEdgeInsets)margin{
    objc_setAssociatedObject(self, uiviewMargin_str, NSStringFromUIEdgeInsets(margin), OBJC_ASSOCIATION_RETAIN);
    
    if(self.superview){//只有在有父視圖的狀況下,才能更新約束
        [self.superview updateConstraints];
    }
}

-(UIEdgeInsets)margin{
    return UIEdgeInsetsFromString(objc_getAssociatedObject(self, uiviewMargin_str));
}


-(void)resetConstraints{
    [self removeConstraints:self.constraints];
}
@end

如今有了這個擴展類就能夠繼續上面的佈局需求了。咱們但願當把UIView添加到superview的時候對該UIView添加各類約束信息。代碼以下:排序

-(void)didAddSubview:(UIView *)subview{
    [super didAddSubview:subview];
    subview.translatesAutoresizingMaskIntoConstraints=NO;//要實現自動佈局,必須把該屬性設置爲no
    [self removeConstraintsWithSubView:subview];//先把subview的原來的約束信息刪除掉
    [self updateSubViewConstraints:subview];//添加新的約束信息
}

上面提到佈局是垂直自上而下的,並且寬度須要隨着屏幕的寬度改變而改變。從這裏咱們能夠得出兩個結論。ip

  1. 寬度上要有一個約束,約束的具體信息是寬度隨着父視圖的寬度變化,還要把間距考慮進去。
  2. 全部添加到同一個父視圖中的subviews按照順序自上而下依序排列。

具體代碼以下

-(void)updateSubViewConstraints:(UIView *)subView{
    UIEdgeInsets margin=subView.margin;
    NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:@"|-left-[view]-right-|" options:0 metrics:@{ @"left" : @(margin.left),@"right":@(margin.right)} views:@{ @"view" : subView}];//添加寬度的約束
    [self addConstraints:constraints];
    //獲取同級下的上一個視圖的,以便作垂直的自上而下排列
    NSInteger index=[self.subviews indexOfObject:subView];
    UIView *preView=index==0?nil:[self.subviews objectAtIndex:index-1];
    if(preView){//若是該subview有排序比它更靠前的視圖
        //該subview的頂部緊靠上一個視圖的底部
        [self addConstraint:[NSLayoutConstraint constraintWithItem:subView
                                                         attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:preView attribute:NSLayoutAttributeBottom multiplier:1.0f constant:(margin.top+preView.margin.bottom)]];
        
    }else{
        //該subview的頂部緊靠父視圖的頂部
        [self addConstraint:[NSLayoutConstraint constraintWithItem:subView
                                                         attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeTop multiplier:1.0f constant:margin.top]];
    }
    
}

至此,咱們已經實現了一個能夠自動自上而下排列的stackpanel。繼續考慮一個問題,若是咱們動態的刪除其中的一個子視圖,咱們會發現全部的約束機會都失效了,爲何?由於從上面的代碼中能夠看出,咱們之因此可以實現自上而下的佈局,徹底是依賴餘有序的先後視圖的各類"依賴約束",也就是NSLayoutConstraint中的relatedBy是上一個視圖,這就比如一條鏈子上的各個有序節點,一旦你把鏈子上的一個節點拿掉,那麼原來的先後關係就改變了,所以約束也就失效了。

而爲了可以實如今UIView從superview中移除的時候不影響整個的約束信息,那麼咱們必須重置約束信息,也就是咱們應該在superview的didRemoveSubview這個方法中來重置,可是很遺憾,沒有這個方法,蘋果只給了咱們willRemoveSubview方法,我目前沒有想到其餘方法,只能在willRemoveSubview這個方法上考慮去實現。如今問題又來了,willRemoveSubview這個方法被調用的時候該subview事實上尚未被刪掉,只是告訴你將要被刪除了。這裏我採用了一個取巧的方法,說實話這樣的代碼不該該出現的,可是沒辦法,只能先將就用下。也就是在willRemoveSubview的方法裏面,再調一次subview的removeFromSuperview的方法,這樣當removeFromSuperview調用完畢的時候就代表該subview已經被移除了,可是這樣一來就會形成循環調用了,所以咱們還須要一個bool參數來標記該subview是有已經被刪除了,所以咱們須要在上面提到的UIPanelSubView類中添加一個不公開的屬性isRemoved,該屬性在UIVIew被添加到superview中的時候設置爲no,被remove的時候設置爲yes。

具體代碼以下:

-(void)didAddSubview:(UIView *)subview{
    [super didAddSubview:subview];
    [subview setIsRemoved:NO];//標記爲未刪除
    subview.translatesAutoresizingMaskIntoConstraints=NO;//要實現自動佈局,必須把該屬性設置爲no
    [self removeConstraintsWithSubView:subview];//先把subview的原來的約束信息刪除掉
    [self updateSubViewConstraints:subview];//添加新的約束信息
}


-(void)willRemoveSubview:(UIView *)subview{
    if(![subview isRemoved]){//由於沒有didRemoveSubView方法,因此只能採用這樣的方式來達到目的了
        [subview setIsRemoved:YES];//標記爲已刪除
        [subview removeFromSuperview];//再調用一次removeFromSuperview,這樣調用完畢該方法,那麼代表該subview已經被移除了
        [self updateConstraints];//重置約束
    }
}
-(void)updateConstraints{
    [super updateConstraints];
    for(UIView * v in self.subviews) {
        [self updateSubViewConstraints:v];
    }
}

這樣就實現了subview被移除的時候仍然能有效約束。

如今當咱們把UIStackPanel添加ViewController的view中的時候,發現旋轉屏幕的時候裏面的佈局沒有跟着變。這是由於咱們以上的約束信息都是UIStackPanel和它的子視圖的,可是UIStackPanel沒有創建起跟它的父視圖的約束,這樣固然不能實現自動佈局啦。要解決這個問題,也很簡單。對UIStackPanel添加一個屬性isBindSizeToSuperView,是否把UIStackPanel的高寬跟父視圖的高寬綁定。

-(void)setIsBindSizeToSuperView:(BOOL)isBindSizeToSuperView{
    if(_isBindSizeToSuperView!=isBindSizeToSuperView){
        _isBindSizeToSuperView=isBindSizeToSuperView;
        if(isBindSizeToSuperView){
            self.translatesAutoresizingMaskIntoConstraints=NO;
            if(self.superview){
                [self bindSizeToSuperView];
            }
        }else{
            self.translatesAutoresizingMaskIntoConstraints=YES;
        }
    }
}


-(void)bindSizeToSuperView{
    UIEdgeInsets margin=self.margin;
    NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:@"|-left-[view]-right-|" options:0 metrics:@{ @"left" : @(margin.left),@"right":@(margin.right)} views:@{ @"view" : self}];
    [self.superview addConstraints:constraints];
    constraints = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|-top-[view]-bottom-|" options:0 metrics:@{ @"top" : @(margin.top),@"bottom":@(margin.bottom)} views:@{ @"view" : self}];
    [self.superview addConstraints:constraints];
}

這樣咱們已經徹底實現了開頭提到的佈局要求。

下面貼出完整的代碼

@interface UIView(UIPanelSubView)
//設置view的大小
@property (nonatomic,assign)CGSize size;
//view距離左上右下的間距
@property (nonatomic,assign)UIEdgeInsets margin;
//重置約束
-(void)resetConstraints;
@end


@interface UIPanel : UIView

@property (nonatomic,assign)BOOL isBindSizeToSuperView;//是否把高寬綁定到父視圖
//更新某個字視圖的約束信息
-(void)updateSubViewConstraints:(UIView *)subView;

//刪除屬於subView的NSLayoutConstraint
-(void)removeConstraintsWithSubView:(UIView *)subView;
@end


@interface UIStackPanel : UIPanel
@property (nonatomic,assign)BOOL isHorizontal;//是否水平佈局
@end
@implementation UIView(UIPanelSubView)
char* const uiviewSize_str = "UIViewSize";
-(void)setSize:(CGSize)size{
    objc_setAssociatedObject(self, uiviewSize_str, NSStringFromCGSize(size), OBJC_ASSOCIATION_RETAIN);
    //先將原來的高寬約束刪除
    for(NSLayoutConstraint *l in self.constraints){
        switch (l.firstAttribute) {
            case NSLayoutAttributeHeight:
            case NSLayoutAttributeWidth:
                [self removeConstraint:l];
                break;
            default:
                break;
        }
    }
    //添加高度約束
    [self addConstraint:[NSLayoutConstraint constraintWithItem:self
                                                     attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0f constant:size.height]];
    //添加寬度約束
    [self addConstraint:[NSLayoutConstraint constraintWithItem:self
                                                     attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0f constant:size.width]];
}

-(CGSize)size{
    return CGSizeFromString(objc_getAssociatedObject(self, uiviewSize_str));
}

char* const uiviewMargin_str = "UIViewMargin";
-(void)setMargin:(UIEdgeInsets)margin{
    objc_setAssociatedObject(self, uiviewMargin_str, NSStringFromUIEdgeInsets(margin), OBJC_ASSOCIATION_RETAIN);
    
    if(self.superview){//只有在有父視圖的狀況下,才能更新約束
        [self.superview updateConstraints];
    }
}

-(UIEdgeInsets)margin{
    return UIEdgeInsetsFromString(objc_getAssociatedObject(self, uiviewMargin_str));
}

//用來標記該視圖一否已經被刪除
char* const uiviewIsRemoved_str = "UIViewIsRemoved";
-(void)setIsRemoved:(BOOL)isRemoved{
    objc_setAssociatedObject(self, uiviewIsRemoved_str, @(isRemoved), OBJC_ASSOCIATION_RETAIN);
}

-(BOOL)isRemoved{
     return [objc_getAssociatedObject(self, uiviewIsRemoved_str) boolValue];
}

-(void)resetConstraints{
    [self removeConstraints:self.constraints];
    if(self.superview && [self.superview respondsToSelector:@selector(updateSubViewConstraints:)]){
        [self.superview performSelector:@selector(removeConstraintsWithSubView:) withObject:self];
        [self.superview performSelector:@selector(updateSubViewConstraints:) withObject:self];
        [self updateConstraints];
    }
}

@end



@implementation UIPanel


-(void)setIsBindSizeToSuperView:(BOOL)isBindSizeToSuperView{
    if(_isBindSizeToSuperView!=isBindSizeToSuperView){
        _isBindSizeToSuperView=isBindSizeToSuperView;
        if(isBindSizeToSuperView){
            self.translatesAutoresizingMaskIntoConstraints=NO;
            if(self.superview){
                [self bindSizeToSuperView];
            }
        }else{
            self.translatesAutoresizingMaskIntoConstraints=YES;
        }
    }
}

-(void)didAddSubview:(UIView *)subview{
    [super didAddSubview:subview];
    [subview setIsRemoved:NO];//標記爲未刪除
    subview.translatesAutoresizingMaskIntoConstraints=NO;//要實現自動佈局,必須把該屬性設置爲no
    [self removeConstraintsWithSubView:subview];//先把subview的原來的約束信息刪除掉
    [self updateSubViewConstraints:subview];//添加新的約束信息
}

-(void)updateSubViewConstraints:(UIView *)subView{
    UIEdgeInsets margin=subView.margin;
    NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:@"|-left-[view]-right-|" options:0 metrics:@{ @"left" : @(margin.left),@"right":@(margin.right)} views:@{ @"view" : subView}];
    [self addConstraints:constraints];
    
    constraints = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|-top-[view]-bottom-|" options:0 metrics:@{ @"top" : @(margin.top),@"bottom":@(margin.bottom)} views:@{ @"view" : subView}];
    [self addConstraints:constraints];
}

-(void)willRemoveSubview:(UIView *)subview{
    if(![subview isRemoved]){//由於沒有didRemoveSubView方法,因此只能採用這樣的方式來達到目的了
        [subview setIsRemoved:YES];//標記爲已刪除
        [subview removeFromSuperview];//再調用一次removeFromSuperview,這樣調用完畢該方法,那麼代表該subview已經被移除了
        [self updateConstraints];//重置約束
    }
}

-(void)removeConstraintsWithSubView:(UIView *)subView{
    for(NSLayoutConstraint *l in self.constraints){
        if(l.firstItem==subView){
            [self removeConstraint:l];
        }
    }
}

-(void)updateConstraints{
    [super updateConstraints];
    for(UIView * v in self.subviews) {
        [self updateSubViewConstraints:v];
    }
}

-(void)bindSizeToSuperView{
    UIEdgeInsets margin=self.margin;
    NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:@"|-left-[view]-right-|" options:0 metrics:@{ @"left" : @(margin.left),@"right":@(margin.right)} views:@{ @"view" : self}];
    [self.superview addConstraints:constraints];
    constraints = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|-top-[view]-bottom-|" options:0 metrics:@{ @"top" : @(margin.top),@"bottom":@(margin.bottom)} views:@{ @"view" : self}];
    [self.superview addConstraints:constraints];
}

-(void)didMoveToSuperview{
    [super didMoveToSuperview];
    if(self.isBindSizeToSuperView){
        [self bindSizeToSuperView];
    }
}
@end




@implementation UIStackPanel

-(void)setIsHorizontal:(BOOL)isHorizontal{
    if(_isHorizontal!=isHorizontal){
        _isHorizontal=isHorizontal;
        [self updateConstraints];
    }
}

-(void)updateSubViewConstraints:(UIView *)subView{
    UIEdgeInsets margin=subView.margin;
    if(self.isHorizontal){
        NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|-top-[view]-bottom-|" options:0 metrics:@{ @"top" : @(margin.top),@"bottom":@(margin.bottom)} views:@{ @"view" : subView}];
        [self addConstraints:constraints];
        
        NSInteger index=[self.subviews indexOfObject:subView];
        UIView *preView=index==0?nil:[self.subviews objectAtIndex:index-1];
        
        if(preView){
            [self addConstraint:[NSLayoutConstraint constraintWithItem:subView
                                                             attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:preView attribute:NSLayoutAttributeRight multiplier:1.0f constant:(margin.left+preView.margin.left)]];
            
        }else{
            [self addConstraint:[NSLayoutConstraint constraintWithItem:subView
                                                             attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeLeft multiplier:1.0f constant:margin.left]];
        }
        
    }else{
        NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:@"|-left-[view]-right-|" options:0 metrics:@{ @"left" : @(margin.left),@"right":@(margin.right)} views:@{ @"view" : subView}];//添加寬度的約束
        [self addConstraints:constraints];
        //獲取同級下的上一個視圖的,以便作垂直的自上而下排列
        NSInteger index=[self.subviews indexOfObject:subView];
        UIView *preView=index==0?nil:[self.subviews objectAtIndex:index-1];
        if(preView){//若是該subview有排序比它更靠前的視圖
            //該subview的頂部緊靠上一個視圖的底部
            [self addConstraint:[NSLayoutConstraint constraintWithItem:subView
                                                             attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:preView attribute:NSLayoutAttributeBottom multiplier:1.0f constant:(margin.top+preView.margin.bottom)]];
            
        }else{
            //該subview的頂部緊靠父視圖的頂部
            [self addConstraint:[NSLayoutConstraint constraintWithItem:subView
                                                             attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeTop multiplier:1.0f constant:margin.top]];
        }
    }
  }
@end

 

下一篇介紹uigridpanel的原理

相關文章
相關標籤/搜索