在上一篇中我提到了如何使用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
具體代碼以下
-(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的原理