1. 程式人生 > >如何做好IOS View的佈局

如何做好IOS View的佈局

這個命題貌似有點大,那就儘量將我理解的分享一下吧,首先說明一點,我是程式碼黨,所以我所講的都是程式碼佈局。本文會圍繞一些我們平常開發中常遇到的佈局問題來進行敘述,包括以下幾個方面:

  • 如何佈局UIViewController的view
  • childViewController的處理
  • Autolayout來佈局
  • tableView管理

1.如何佈局UIViewController的view

首先給出設計原則:

    1. 螢幕尺寸變化時能自適應,如不同尺寸裝置,螢幕旋轉,熱點,電話等。
    1. 無論是否有navigationBar或tabBar都能夠正常顯示,即要考慮是否有這些bar的所有場景
    1. 儘量避免hard code間距,如20,44,49等

1.1 是否全屏

自從ios7扁平化設計以來,高斯模組是為你的應用增色的很好的工具,而為了更好的讓navigationBar和tabBar實現高斯模糊的效果,最好讓UIViewController能夠全屏佈局。我們在設計一個頁面時,最好先確定好是否需要全屏佈局,確定了這一點,我們就很簡單的這樣設定來決定是否全屏佈局。

不需要全屏佈局:

1
self.edgesForExtendedLayout = UIRectEdgeNone;

需要全屏佈局:

1
2
self.extendedLayoutIncludesOpaqueBars = YES;
// 在ios7.1以後,ios會根據navigationBar的translucent來自動確定是否全屏。

1.2 subview的佈局

對於subview的frame設定不難,重要的是要做到以下2點:

  1. 在ViewController的view尺寸變化時能自適應,如螢幕旋轉,熱點,電話等。
  2. 無論是否有navigationBar或tabBar都能夠正常顯示

做到第一點不難,不使用Autolayout也可以做到,那就是設定view的 autoresizingMask, 這個屬性在還是frame佈局時代是適配的利器。比如這樣設定就可以讓subview始終和view的尺寸一致:

1
2
tableView.frame = self.view.bounds;
tableView.autoresizingMask
= UIViewAutoresizingFlexibleHeight|UIViewAutoresizingFlexibleWidth;

這個的詳細使用可以參考Autoresizing這裡面的介紹,本文不詳細描述。

但是要做到第二點不使用Autolayout就有點捉襟見肘了,因為ios7以上全屏佈局到處都是,為了能更好適配不至於navigationBar或者tabBar擋住了內容,當然你可以說不需要全屏,但是有一種情況你還要考慮:

在沒有navigationBar的情況下,statusBar的高度不被考慮,於是你又不得不做出判斷,當沒有navigationBar的情況下,view上面留20畫素來避免被statusBar擋住。

那如何來做呢,答案就是使用 LayoutGuide, 例如這個:

1
2
3
4
5
6
[view mas_makeConstraints:^(MASConstraintMaker *make) {
    make.top.mas_equalTo(self.mas_topLayoutGuideBottom);
    make.leading.mas_equalTo(self.view.mas_leading);
    make.trailing.mas_equalTo(self.view.mas_trailing);
    make.bottom.mas_equalTo(self.mas_bottomLayoutGuideTop);
}];

本文autolayout均使用 Masonry 作為示例,如果你不瞭解Masonry,請參考Masonry

上面的示例程式碼就保證了view不會被頂部或者底部的“條”遮住。

1.3 scrollView的contentInset

上面一小節的例子說明了如何在view是全屏的時候如何佈局subview不被擋住, 但是如果我的subview是 UIScrollView, 也要全屏佈局呢(為了炫酷的高斯模糊效果), 一般的做法就是設定 contentInset來避免內容被擋住, 這裡我的答案是設定automaticallyAdjustsScrollViewInsets, 你是不是想:不是逗我吧,這東西不是坑麼,很容易不起作用的。下面我來解釋下。

為什麼要使用automaticallyAdjustsScrollViewInsets就是我們要依從上面原則中的b. 不依賴當前是否有navigationBar或者tabBar來hardCode佈局subview。 當然你也可以通過判斷當前是否有navigationBar或者tabBar來手動計算,但是這樣不是感覺有點dirty麼。

蘋果設計UIViewController的automaticallyAdjustsScrollViewInsets這個屬性就是為了應對scrollView的全屏佈局的,會依據viewController所處的環境(是否有navigationBar或者tabBar之類的bar), 在UIViewController的view moveToWindow的時候,自動設定scrollView的 contentInset 和scrollIndicatorInsets來保證內容不被擋住, 但完美使用且不出問題要滿足以下的條件:

  • UIScrollView是 UIViewConroller的view的第一個subview.
  • UIScrollView的位置正好是view的bounds

我想說的是,我們完全可以滿足上面兩點要求,經過我實踐的經驗,如果你的佈局設計稿滿足不了以上兩點要求的頁面,這樣的頁面也沒有什麼需要全屏的必要,這種情況下就不要全屏就好了嘛。當然如果你還是會有這樣的需求,也有方法,參考2.2

1.4 SCREEN_WIDTH 和 SCREEN_HEIGHT

相信很多同學的工程裡面一定可以搜到這兩個巨集,或者類似的東西,即是在我們使用了Autolayout佈局之後,某些場景可能還是會讓我們使用到這兩個引數。比如設定 preferredMaxLayoutWidth(IOS7,IOS8已經可以自動) 的時候,很多時候直接用SCREEN_WIDTH來計算。

但是這兩個東西也算是HARD CODE啊,只是目前用起來還好,其實也應該摒棄,像ipad分屏出來以後,這個東西就成了麻煩的事情了,但是麻煩我們也要解決啊,不然久而久之這個一定會是個坑的。

思考為了移除 SCREEN_WIDTH 和 SCREEN_HEIGHT, 我們需要兼顧哪些事情呢?

  • 儘量依賴相對關係來計算size
  • 類似 preferredMaxLayoutWidth 這樣的屬性也要去除依賴
  • 在view的size變化時,preferredMaxLayoutWidth可以對應更改

上面做到第一點應該不難,用Autolayout完全滿足,第二點和第三點可以給個參考,在view的layoutSubviews裡面根據當前的size大小來設定 preferredMaxLayoutWidth,有人實際操作過可行,在IOS7上也實現了 Automatic 的 preferredMaxLayoutWidth

1.5 IOS6問題

本來也想講講IOS6的相容,但是現在淘寶天貓都開始不相容IOS6了,還費精力幹啥,把精力放到更有意義的事上吧,如果你們老闆還在要相容IOS6,你就開啟淘寶天貓最新版,說:看!淘寶都不相容了。

2. childViewController

在開發過程中,發現還有小部分同學對 childViewController的用法是錯誤的,你是否見過這樣的程式碼呢?

1
2
3
4
5
TestViewController *vc = [[TestViewController alloc] init];
[vc willMoveToParentViewController:self];
[self addChildViewController:vc];
[self.view addSubview:vc.view];
[vc didMoveToParentViewController:self];

其實這裡面第二行是多餘的,具體如何使用,蘋果的註釋裡面很清楚了:

These two methods are public for container subclasses to call when transitioning between child controllers. If they are overridden, the overrides should ensure to call the super. The parent argument in both of these methods is nil when a child is being removed from its parent; otherwise it is equal to the new parent view controller.

addChildViewController: will call [child willMoveToParentViewController:self] before adding the child. However, it will not call didMoveToParentViewController:. It is expected that a container view controller subclass will make this call after a transition to the new child has completed or, in the case of no transition, immediately after the call to addChildViewController:. Similarly removeFromParentViewController: does not call [self willMoveToParentViewController:nil] before removing the child. This is also the responsibilty of the container subclass. Container subclasses will typically define a method that transitions to a new child by first calling addChildViewController:, then executing a transition which will add the new child’s view into the view hierarchy of its parent, and finally will call didMoveToParentViewController:. Similarly, subclasses will typically define a method that removes a child in the reverse manner by first calling [child willMoveToParentViewController:nil].

大概意思是: 1. 過載 willMoveToParentViewController: 和 didMoveToParentViewController:時不要忘了super 2. addChildViewController的時候只需要在後面呼叫 didMoveToParentViewController: 3.removeChildViewController的時候只需要在前面 willMoveToParentViewController: 4. 如果有切換動畫應該先 addChildViewController 再將view新增到parentView上並做切換動畫,動畫結束再didMoveToParentViewController:

好了,今天講的是 childViewController裡面的佈局問題,同樣會遇到上面的問題:

2.1 childViewController的layoutGuide

在 childViewController 中使用 layoutGuide 不那麼好使了, IOS7上完全不對,IOS9上可以正常工作,IOS8還沒有測。那如何解決呢,解決方案可以取其 parentViewController的layoutGuide嘛,參考toplayoutguide-in-child-view-controller

當然取出來的 layoutGuide 可不能直接在Autolayout裡面使用了,但是可以取其 length 來進行使用。

2.2 childViewController的 contentInset

在 childViewController 中 automaticallyAdjustsScrollViewInsets 也沒用了, 解決方案可以同 2.1 取其 parentViewController的 layoutGuide 的length來進行設定, 這裡面有個細節要注意,就是設定 contentInset 的方法放在 viewWillLayoutSubviews 函式裡面才最佳。

3. Autolayout

上面講了一些關於 viewController 怎麼處理佈局的問題,下面就列舉一些實用佈局的例項來解釋如何用Autolayout來佈局,已經其好處和必要性。

3.1 組合區塊

這一節我們舉例一個簡單的區塊佈局,常見一些電商類活動資源模板建立。需求如下:

   _______________________________________________________________________
  |  _______________3________________   ________________3_______________  |
  | |                                | |                                | |
  | |                                | |                                | |
  | |                                | |                                | |
  | |                                | |                                | |
  | |                                | |             view2              | |
  | |                                | |                                | |
  | |                                | |                                |3|
  | |                                |3|                                | |
  | |           view1                | |________________________________| |
  |3|                                |  _______3______   _______3_______  |
  | |                                | |              | |               | |
  | |                                | |              | |               | |
  | |                                | |              | |               | |
  | |                                | |              | |               | |
  | |                                | |    view3     | |     view4     | |
  | |                                | |              | |               | |
  | |                                | |              | |               | |
  | |                                | |              | |               | |
  | |________________________________| |______________| |_______________| |
  |_____________3__________________________________3______________________|

其中 view1 和 view2 同寬, view2 和 view3view4 同高, view3 和 view4 同寬, 所有的margin都是3。要完成這樣要求的佈局,可以很容易的用Autolayout來完成, 只需要指定好這些間距和寬度的關係就好了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
    make.leading.mas_equalTo(superview.mas_leading).offset(3);
    make.top.mas_equalTo(superview.mas_top).offset(3);
    make.bottom.mas_equalTo(superview.mas_bottom).offset(-3);
    make.width.mas_equalTo(view2.mas_width);
}];
[view2 mas_makeConstraints:^(MASConstraintMaker *make) {
    make.leading.mas_equalTo(view1.mas_trailing).offset(3);
    make.top.mas_equalTo(superview.mas_top).offset(3);
    make.trailing.mas_equalTo(superview.mas_trailing).offset(-3);
    make.height.mas_equalTo(view3.mas_height);
}];
[view3 mas_makeConstraints:^(MASConstraintMaker *make) {
    make.leading.mas_equalTo(view2.mas_leading).offset(3);
    make.width.mas_equalTo(view4.mas_width);
    make.top.mas_equalTo(view2.mas_bottom).offset(3);
    make.bottom.mas_equalTo(superview.mas_bottom).offset(-.offset(3)3);
}];
[view4 mas_makeConstraints:^(MASConstraintMaker *make) {
    make.leading.mas_equalTo(view3.mas_trailing).offset(3);
    make.trailing.mas_equalTo(view2.mas_trailing);
    make.top.mas_equalTo(view2.mas_bottom).offset(3);
    make.bottom.mas_equalTo(superview.mas_bottom).offset(-3);
}];

如何正確的設定 Constraints 的原則就是:

Contraints 設定的條件滿足可以計算出view的frame.

相信大家對於這點數學基礎應該沒問題的。明白則個基本原則後,然後就要了解ios系統框架已經做了哪些事情,不然你可能不知道條件已經足夠了,比如我們即將講到的 UILabel

3.2 UILabel

之所以把 UILabel 單拿出來講,是因為其佈局的特殊性,可能需要根據內容來決定其真實view的size.

3.2.1 content Hugging & content Compression

有沒有發現,你放一個 UILabel 在view上,然後只新增2個Constraint(top and leading), 這個Label也可以正常顯示,也沒有報警告或者crash, 這是為什麼呢,列印一下發現 label 上有兩個 constraint:

1
2
3
4
5
(lldb) po [0x14664e5a0 constraints]
<__NSArrayI 0x1466100b0>(
<NSContentSizeLayoutConstraint:0x146563690 UILabel:0x14664e5a0.width == 41.6667>,
<NSContentSizeLayoutConstraint:0x1465637a0 UILabel:0x14664e5a0.height == 20.3333>
)

這就是 content Hugging 或 content Compression 起作用了,api如下:

1
2
3
4
5
- (UILayoutPriority)contentHuggingPriorityForAxis:(UILayoutConstraintAxis)axis NS_AVAILABLE_IOS(6_0);
- (void)setContentHuggingPriority:(UILayoutPriority)priority forAxis:(UILayoutConstraintAxis)axis NS_AVAILABLE_IOS(6_0);
- (UILayoutPriority)contentCompressionResistancePriorityForAxis:(UILayoutConstraintAxis)axis NS_AVAILABLE_IOS(6_0);
- (void)setContentCompressionResistancePriority: