天天看點

如何做好IOS View的布局

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

如何布局uiviewcontroller的view

childviewcontroller的處理

autolayout來布局

tableview管理

首先給出設計原則:

螢幕尺寸變化時能自适應,如不同尺寸裝置,螢幕旋轉,熱點,電話等。

無論是否有navigationbar或tabbar都能夠正常顯示,即要考慮是否有這些bar的所有場景

盡量避免hard code間距,如20,44,49等

自從ios7扁平化設計以來,高斯子產品是為你的應用增色的很好的工具,而為了更好的讓navigationbar和tabbar實作高斯模糊的效果,最好讓uiviewcontroller能夠全屏布局。我們在設計一個頁面時,最好先确定好是否需要全屏布局,确定了這一點,我們就很簡單的這樣設定來決定是否全屏布局。

不需要全屏布局:

需要全屏布局:

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

在viewcontroller的view尺寸變化時能自适應,如螢幕旋轉,熱點,電話等。 無論是否有navigationbar或tabbar都能夠正常顯示

做到第一點不難,不使用autolayout也可以做到,那就是設定view的 <code>autoresizingmask</code>, 這個屬性在還是frame布局時代是适配的利器。比如這樣設定就可以讓subview始終和view的尺寸一緻:

但是要做到第二點不使用autolayout就有點捉襟見肘了,因為ios7以上全屏布局到處都是,為了能更好适配不至于navigationbar或者tabbar擋住了内容,當然你可以說不需要全屏,但是有一種情況你還要考慮:

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

那如何來做呢,答案就是使用 <code>layoutguide</code>, 例如這個:

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

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

為什麼要使用<code>automaticallyadjustsscrollviewinsets</code>就是我們要依從上面原則中的b. 不依賴目前是否有navigationbar或者tabbar來hardcode布局subview。 當然你也可以通過判斷目前是否有navigationbar或者tabbar來手動計算,但是這樣不是感覺有點dirty麼。

蘋果設計uiviewcontroller的<code>automaticallyadjustsscrollviewinsets</code>這個屬性就是為了應對scrollview的全屏布局的,會依據viewcontroller所處的環境(是否有navigationbar或者tabbar之類的bar), 在uiviewcontroller的view movetowindow的時候,自動設定scrollview的 <code>contentinset</code> 和 <code>scrollindicatorinsets</code>來保證内容不被擋住, 但完美使用且不出問題要滿足以下的條件:

uiscrollview是 uiviewconroller的view的第一個subview.

uiscrollview的位置正好是view的bounds

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

相信很多同學的工程裡面一定可以搜到這兩個宏,或者類似的東西,即是在我們使用了autolayout布局之後,某些場景可能還是會讓我們使用到這兩個參數。比如設定 <code>preferredmaxlayoutwidth</code>(ios7,ios8已經可以自動) 的時候,很多時候直接用screen_width來計算。

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

思考為了移除 <code>screen_width</code> 和 <code>screen_height</code>, 我們需要兼顧哪些事情呢?

盡量依賴相對關系來計算size

類似 <code>preferredmaxlayoutwidth</code> 這樣的屬性也要去除依賴

在view的size變化時,<code>preferredmaxlayoutwidth</code>可以對應更改

上面做到第一點應該不難,用autolayout完全滿足,第二點和第三點可以給個參考,在view的<code>layoutsubviews</code>裡面根據目前的<code>size</code>大小來設定 <code>preferredmaxlayoutwidth</code>,有人實際操作過可行,在ios7上也實作了 <code>automatic</code> 的 <code>preferredmaxlayoutwidth</code>。

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

在開發過程中,發現還有小部分同學對 <code>childviewcontroller</code>的用法是錯誤的,你是否見過這樣的代碼呢?

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

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. 重載 <code>willmovetoparentviewcontroller:</code> 和 <code>didmovetoparentviewcontroller:</code>時不要忘了super 2. <code>addchildviewcontroller</code>的時候隻需要在後面調用 <code>didmovetoparentviewcontroller:</code> 3. <code>removechildviewcontroller</code>的時候隻需要在前面 <code>willmovetoparentviewcontroller:</code> 4. 如果有切換動畫應該先 <code>addchildviewcontroller</code> 再将<code>view</code>添加到parentview上并做切換動畫,動畫結束再<code>didmovetoparentviewcontroller:</code>

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

當然取出來的 layoutguide 可不能直接在autolayout裡面使用了,但是可以取其 <code>length</code> 來進行使用。

在 <code>childviewcontroller</code> 中 <code>automaticallyadjustsscrollviewinsets</code> 也沒用了, 解決方案可以同 2.1 取其 parentviewcontroller的 layoutguide 的length來進行設定, 這裡面有個細節要注意,就是設定 contentinset 的方法放在 <code>viewwilllayoutsubviews</code> 函數裡面才最佳。

上面講了一些關于 viewcontroller 怎麼處理布局的問題,下面就列舉一些實用布局的執行個體來解釋如何用autolayout來布局,已經其好處和必要性。

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

其中 <code>view1</code> 和 <code>view2</code> 同寬, <code>view2</code> 和 <code>view3</code>, <code>view4</code> 同高, <code>view3</code> 和 <code>view4</code> 同寬, 所有的margin都是3。要完成這樣要求的布局,可以很容易的用autolayout來完成, 隻需要指定好這些間距和寬度的關系就好了。

如何正确的設定 <code>constraints</code> 的原則就是:

<code>contraints</code> 設定的條件滿足可以計算出view的frame.

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

之是以把 <code>uilabel</code> 單拿出來講,是因為其布局的特殊性,可能需要根據内容來決定其真實view的size.

有沒有發現,你放一個 <code>uilabel</code> 在view上,然後隻添加2個constraint(top and leading), 這個label也可以正常顯示,也沒有報警告或者crash, 這是為什麼呢,列印一下發現 label 上有兩個 constraint:

這就是 content hugging 或 content compression 起作用了,api如下:

在xib中看起來是這樣的

如何做好IOS View的布局

這兩個屬性是當 uiview 的 <code>intrinsiccontentsize</code> 不為 <code>uiviewnointrinsicmetric</code> 的時候, 分别代表 ① view的邊緣緊緊貼着它的内容 ② view的不透明的内容不會被壓縮或者截斷。

這兩個屬性不光是uilabel, uiimageview, uibutton等都可以使用。知道這兩個屬性對我們在同向多個view布局時選擇如何布局有很重要的作用。

在以往的節目開發中我們經常會要預先計算 text文本的高度,然後再根據計算出來的結果設定uilabel的frame,在純文字時代做起來順風順水。但是進入富文本時代後,你會發現一個問題:text的size計算不準 。 而且現在富文本的需求,比比皆是,uilabel使用富文本也在ios6之後變的easy. 但是如果還是用老的方案來設定uilabel的frame, 你會發現總有瑕疵,很可能發現文本高度計算不對,被截掉内容了。即使用系統的新api: <code>-boundingrectwithsize:options:attributes:context:</code>, 也存在在計算不準的情況, 我就遇到過,當設定文本段的 <code>firstlineheadindent</code> 之後,計算出來的size也會發生有偏差的情況下。

那麼,有了autolayout之後,既然計算不準,而且又要寫一堆計算高度的代碼(比如計算cell高度),增加了代碼的閱讀複雜度和可維護度,那就不計算了,讓autolayout自己算出來!

使用上面的思路,我們可以在cell裡面設定好constraints之後,先預填一下内容,讓cell自己先布局一遍,拿到計算的結果儲存起來,避免二次計算。那麼我們就可以實作cell高度的自動計算了,既省事,又準确,隻是使用時要設計方案避免多餘的性能損耗。

autolayout分享到這吧,我前面也已經寫過2篇關于autolayout的了,這裡就不多重複了。

tableview 作為ios開發最常用的頁面控件,可以說是大家最熟悉不過的了,今天我們讨論一個大家都有類似體會的經曆:

step1:

我們接到一個需求:要做一個展示商品或文章詳情的頁面,第一行展示圖檔,第二行展示名稱介紹價格等,第三行展示相關權益,第四行選擇商品簇,第五行展示評價。

我們先假設用了最沒有設計的做法:就是在tableview的datasource和delegate方法裡面直接if/else判斷:

ok, 開發完了,上線,工作良好。

step2:

二期,pd說我們要做搶購,那這個商品頁如果是搶購商品的話 第二行要加一行 搶購相關資訊,比如倒計時,這個時候,這個時候再想想 step1 的設計,還要繼續嗎,試想一下要如何 if/else才能解決?最終肯定是發現再這樣搞就成了一坨了,那麼這樣的設計顯然不合理, 就算加班搞定了,那以後再變更呢,這個坑隻能越來越大。那如何重構呢?

其實這種情況是因為rpc擷取的model并不是我們最終用來布局tableview所需要的 viewmodel. 我們需要根據tableview的特性抽象出來一個<code>[{section:[row,row,row...]},{section:[row,row,row...]}, ...]</code>這樣結構的<code>tablemodel</code>, 然後我們就可以先将 <code>model</code> 解析為這個 <code>tablemodel</code>, 然後根據這個 <code>tablemodel</code> 來布局 tableview. 這樣設計以後,我們就把業務邏輯和布局邏輯分開了,甚至都不用修改 tableview 的代理方法了。

step3:

經過step2的重構,以後再改動需求過來,修改界面邏輯就不那麼負責了,但是思考下日常中遇到這種情況的頁面不在少數,那麼為什麼不把 <code>tablemodel</code> 這一塊封裝成元件呢,畢竟tableview的布局都是類似的。

那麼封裝元件的話,我們要思考做到哪些設計:

低侵入性,不希望還要重載 <code>uitableview</code>, 或者 tabelview的 <code>datasource</code>或<code>delegate</code>必須指向某個對象,這些設計對于原生的改造成本太高,而且造成了耦合,不利于維護。

tablemodel 自身可以友善的進行編輯, 比如增删改查 sectionmodel, 增删改查 rowmodel, 根據indexpath查找model,和根據model查詢indexpath都友善。

能夠基本做到tableview布局變化不用改動 datasource 方法

能夠緩存cell高度,避免重複計算

實作autolayout的cell自動計算高度并緩存。

本文對一些ios布局上做了一些總結,提出了一些建議和原則,希望可以幫助到一部分人,當然如果有錯誤,歡迎指正。

繼續閱讀