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


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

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

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

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

代碼如下:

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

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

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

@interface UIView(UIPanelSubView)
//設置view的大小
@property (nonatomic,assign)CGSize size;
//view距離左上右下的間距
@property (nonatomic,assign)UIEdgeInsets margin;
//重置約束
-(void)resetConstraints;
@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));
}


-(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];//添加新的約束信息
}

上面提到布局是垂直自上而下的,而且寬度需要隨着屏幕的寬度改變而改變。從這里我們可以得出兩個結論。

  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的原理

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM