IOS架構設計(解耦的嘗試)之UI樣式復用與布局管理
該系列文章是2016年折騰的一個總結,對於這一年中思考和解決的一些問題做一些梳理和總結。Talk is cheap show me the code.
本來只是想寫寫ElementKit中對於MVVM的實踐來著,結果發現這一年做的一些事情中,還有值的繼續說的。而且基本上都是圍繞著解耦和復用的主題。而且很多都是在日常的開發中常見的問題和其解決方案。也就繼續寫寫,算是拋磚引玉,在悶頭設計開發之後與業界交流一下。
這一次來說一下在日常的開發中最常見的兩個問題:
一個是樣式,顏色、背景、font。。。。一個是布局管理其中的成果自然也是搞成了類庫方便使用(Talk is cheap show me the code.):StyleSheet和DZGeometryTools。
樣式和布局基本上是在IOS編程中最繁重的兩個工作,之所以說繁重,
一是因為常見,絕大多數IOSCoder每寫一個業務邏輯,都得和這兩個事情糾纏上半天,去還原UI/UE同學的設計。二是因為UI樣式千變萬化很難像其他業務邏輯那樣用類繼承了之類的策略來處理。每次都得蠻干。再一次提一句話『懶是第一生產力』,有壓迫的地方就有反抗,有繁重的地方就想著偷懶。所以得需要一個有效途徑來降低UI工作的繁重。
抽象的威力其實我一直希望自己的文章能夠道業雙關。在說明一個問題的解決方案的同時,也能夠解釋清楚自己設計一個類庫或者做一個技術決定背後的東西:知識背景和思維方法。還原整個從問題發現到問題解決方案的過程。所以,發現之前的幾篇文章中多多少少有一些東扯西扯的東西。自然這篇文章也難免其俗:)。這一次要扯的是抽象。之所以要說這個是因為,在解決UI工作繁重的問題中,就是用的這個最基本的方法。
刪繁就簡三秋樹,霜葉紅於二月花
從小老師就耳提面命:要看透問題的表象看本質。起初不解,隨著干Coder時間長了,愈發察覺要想偷懶就得去解決本質問題,而不是圍繞著問題的表層轉來轉去。而進一步說,不能停留在使用簡單抽象出來的概念解決問題的層次,應該進一步使用一些高級抽象。就拿布局這個事情來說吧。
原始抽象第一層的抽象是對現實世界在數字世界的描述,用坐標系統。用數學語言來描述布局這個事情。其實抽象到這一步也是一個非常大的進步了。在iOS中也就是我們常用的spring&struct的布局系統。通過frame等來標注一個view來控制平面布局,通過View-Tree來控制Z軸方向上的布局。
基於這種抽象的情況下,我們做的事情就是精准的描述每一個View在坐標系統中的位置。這個就是那個繁重的工作。在
描述抽象layoutsubviews
函數裡面裡面,各種計算大小和偏移量。熟悉iOS的同學,隨意看一下自己View裡面layoutSubviews
函數的大小就可以感受到其繁重。抽象這個東西有意思的地方就在於他是可以遞歸使用的。在第一層抽象得到的概念基礎上,進行第二層抽象;在第二層抽象得到的概念基礎上,進行第三層抽象…..如此反復。當然,Apple牛逼的工程師們也會意識到spring&struct的繁重,於是就有了Autolayout(自動布局系統)。
Autolayout是基於描述的抽象,更多的是描述元素與元素之間的位置關系。通過解n元一次方程組來確定元素的位置(在坐標系統中的frame)。而這種基於模式的抽象,要比剛才第一層的抽象要高級很多。在這種抽象概念下,我們發現我們寫的布局的代碼有了一個大幅度的減少。原先需要拼了老命計算view frame的情況變成了,描述view之間關系的過程。原先需要非常多代碼才能完成的任務,現在只要簡單的寫一句AView在x軸方向上距離BView固定間隔為10就可以完成了。隨著代碼量的降低,我們的對於布局系統理解成本都在降低。
復雜性由固有復雜性和認知復雜性組成。
而基於描述的抽象,因為是對第一層抽象概念的操作,忽略了第一層抽象的很多細節,很大程度上降低了布局系統的認知復雜度。無論從代碼量上還是我們的認知成本上,都讓我們可以『偷懶』了。
任務抽象而有了基於描述抽象的Autolayout之後,我們會發現其實很多任務還是很繁重,比如我們要寫個List布局,不可能每次都把List中每個元素的位置關系描述一邊啊。比如我們要寫個Collection的布局,也不可能每次都把每個元素的位置關系都描述一邊啊…..
於是真對某些特定任務,會使用任務抽象。把List布局抽象成UITableView,把Collection布局抽象成UICollectionView,把線性布局抽象成UIStackView。。。。。而且Apple的工程師們也會在這條路上越走越遠。在近幾個版本的iOS-SDK更新中,我們也看到了更多的這種布局View的出現。相信以後也會更多。
而這種類型的View在簡化編程工作這件事情上要比autolayout來的更加厲害。可以隨意感受一下,我原先為了實現一個TableView所寫的代碼量DZTableView。就知道當我們把通用任務抽象出來的時候,能偷多大的懶了。
而關於這種任務抽象我們聽到的最多就是面試中經常被問及的GoF設計模式。在想想應用設計模式時的爽,也就知道抽象這個工具的確很好用。在這裡強烈推薦一本書《元素模式》。個人認為這本提供了一整套的瑞士軍刀,來幫你進行抽象或者去設計『設計模式』。
….當然這裡還會存在更高層次的抽象。不過嘛,抽象並非銀彈。並不是說一味的抽象下去就能夠得到極致的『偷懶』。隨著抽象層次提高,概念密度也在提高,而那些在這個過程中所忽略的特殊場中的細節,將變得難以還原。有些時候,處理問題反而更加困難,編碼量卻在增加。老生常談的問題啊:度。當抽象層次滿足業務需求和業務發展的時候,就可以臨時先止步了。
DZGeometryTools簡單描述了一下抽象的威力之後,開始切入如何使用部分。順著剛才的話題講布局的事情。我自己寫界面的時候也是盡可能的”偷懶”。於是就有了這個庫DZGeometryTools。主要是用了描述抽象和任務抽象兩種技術。看起來忽悠人,其實實現部分一看就明白了。無非是將常用的一些任務轉換成了函數而已。其實在
DZGeometryTools.h中主要是對幾何類型的操作CoreGraphics
框架中又一個CGGeometry.h
文件,其中提供了很多方便操作CGRect等幾何類型的函數。而在DZGemetryTools中所做的工作可以看做是對CGGeometry的一個擴展。通過抽象我將比較常見的幾何操作歸結為以下幾種比較基礎的操作:
偏移操作縮放操作間距計算操作margin施加操作現在把他們提取出來,基本上就是完成了一個工具集的構建。而後在這些工具集上,就可以來描述每個Rect之間的關系,這個有點類似於autolayout,不過是提前收工算好了,而autolayout是自動計算並賦值的。貼段真實使用中代碼來感受一下:
- (void) layoutSubviews { [super layoutSubviews]; CGSize imageSize = {62*2, 84}; CGRect contentRect = CGRectCenterSubSize(self.bounds, CGSizeMake(20, 20)); CGRect textRect; CGRect imageRect; CGRectDivide(contentRect, &textRect, &imageRect, 30, CGRectMaxYEdge); imageRect = CGRectCenter(imageRect, imageSize); CGRect imgRs[2]; CGRectHorizontalSplit(imageRect, imgRs, 2, 0); _indicatorImageView.frame = imgRs[0]; _powerImageView.frame = imgRs[1]; _textLabel.frame = textRect; _backgroundView.frame = self.bounds; _lastTimeLabel.frame = imageRect; }
其中使用到了
DZLayoutMacros.h 常用的任務CGRectCenterSubSize
來對contentRect做了margin計算,並用CGRectHorizontalSplit
對rect坐了縱向均分的操作,這些都是描述UI元素之間相對位置關系的關系,我們通過手工計算來完成了對於UI元素的布局操作。這些函數都沒有采用直接操作view.frame的方式,只進行了幾何運算,計算出了UI元素坐標。之所以采取這種方式,是因為想基於目前的抽象層次應該是針對於幾何概念的操作。對於UI元素的操作,已經簡化成了一個賦值操作,沒有太大必要去優化處理了。而相較於以前的編碼量和思維量來說,基於目前構建的這個工具集來進行編碼已經省了不少力氣了。體力活少了。在這個文件裡面依舊是一些常用的布局工具集,不過和上面不一樣的是采用了另外的表達方式:
y依賴於頂部元素,並且盡可能填充滿width的布局頂部固定高度,鋪滿width的布局//底部固定高度,鋪滿width的布局….. 小結宏
。針對於一些比較簡單的任務,做了抽象。比如:這些函數是我在日常的編碼中抽象出來的一些比較基礎的工具。沒有完整的模型,是比較零散的收集吧,更多的東西還是看庫中代碼的注釋DZGeometryTools。這是個遺憾,沒有統一的模型。一直都想搞一個類似於autolayout或者flexbox之類的布局系統,能夠更加方面的來完成布局的操作。這個工作看來得放到17年做了,惟願完成。
StyleSheet據說一個終端開發人員將會有70%以上的時間在和UI打交道。自己想想也對,貌似有很大一部分時間花費在了調整UI樣式,addSubView還有layout上面。猛然間就發現自己的代碼中有大量這種東西存在
self.label.layer.cornerRadius = 3; self.label.textColor = [UIColor darkTextColor]; self.label.font = [UIFont systemFontOfSize:13]; self.label.backgroundColor = [UIColor greenColor]; self.label.layer.borderWidth = 2; self.label.layer.borderColor = [UIColor redColor].CGColor; self.label2.layer.cornerRadius = 3; self.label2.textColor = [UIColor darkTextColor]; self.label2.font = [UIFont systemFontOfSize:13]; self.label2.backgroundColor = [UIColor greenColor]; self.label2.layer.borderWidth = 2; self.label2.layer.borderColor = [UIColor redColor].CGColor; self.button.layer.cornerRadius = 3; self.button.backgroundColor = [UIColor greenColor]; self.button.layer.borderWidth = 2; self.button.layer.borderColor = [UIColor redColor].CGColor; self.aView.layer.cornerRadius = 3; self.aView.backgroundColor = [UIColor greenColor]; self.aView.layer.borderWidth = 2; self.aView.layer.borderColor = [UIColor redColor].CGColor; ......
上面的代碼是為了實現這樣的效果而寫的代碼。
很多幾乎是一毛一樣的代碼,充斥著整個APP。自己花在這些樣式調整上的時間也非常多。為了實現一個樣式效果,需要配置各種各樣的屬性。而且很多界面中這些樣式都是一樣的。於是又是無數次的重復上面的工作。oy my god!時間啊,就這樣流走了。做為一個懶人,就會發問有沒有一種可以少寫點代碼的方式呢?你可以寫一個子類嘛,但是會有類污染的問題,單純為了一個公有樣式,就創建個子類有點大材小用。那寫一批樣式渲染的函數呗,恩這個注意不錯,但是細想一下工作量也不小,而且不通用。於是,花了幾天的時間我寫了StyleSheet這個庫。為了的就是來簡化UI樣式的編碼。
通過上述描述我們可以發現,原始的寫UI樣式的問題:
繁瑣的代碼,大量重復性的工作樣式無法共享,每一個View都需要重新進行樣式賦值。而StyleSheet的設計目標就是:
樣式配置輕便化,能夠使用更加少的代碼來描述View的樣式樣式在View之間的共享.不止是相同類的實例之間的共享,甚至是跨類的共享。So,先看看上述代碼使用StyleSheet之後的效果:
設計與使用self.label.style = DZLabelStyleMake( style.backgroundColor = [UIColor greenColor]; style.cornerRedius = 3; style.borderColor = [UIColor redColor]; style.borderWidth = 2; style.textStyle.textColor = [UIColor darkTextColor]; style.textStyle.font = [UIFont systemFontOfSize:13]; ); self.label2.style = self.label.style; self.aView.style = self.label.style; [self.button.style copyAttributesWithStyle:self.label.style];
基礎抽象模型很簡單,就是要讓界面上關於展示的屬性可以被組合使用。而我們所謂的樣式,其實也就是各種基礎屬性組合出來的結果。基於這個模型,在設計StyleSheet的時候故意淡化了被渲染的View的類型的概念,任何一種類型的Style可以對任何類型的View進行渲染,但是必須是這種類型的View支持Style所指稱的屬性。比如你可以使用真對Button設計的DZButtonStateStyle來渲染一個UILabel,但由於UILabel不支持DZButtonStateStyle中的渲染屬性,所以渲染結果是無效的。
但是當使用DZButtonStyle(繼承自DZViewStyle)來渲染UILabel的時候,會使用DZButtonStyle中其父類的某些渲染屬性,來渲染UILabel的父類UIView所支持的那些屬性。
使用 直接使用Style對View進行渲染:DZLabelStyle* style = DZLabelStyleMake( style.backgroundColor = [UIColor greenColor]; style.cornerRedius = 3; style.borderColor = [UIColor redColor]; style.borderWidth = 2; style.textStyle.textColor = [UIColor darkTextColor]; style.textStyle.font = [UIFont systemFontOfSize:13]; ); [style decorateView:self.label];
直接渲染的好處是,不用再次生成Style對象,更加方便樣式在多個View之間渲染。
賦值渲染對UIKit中常用的一些組件進行了擴張為他們增利了style屬性,直接進行style屬性的賦值,會出發一次渲染操作。當第一次調用style屬性的時候,會自動生成一個zeroStyle並賦值。 self.label.style = style; 或者 self.label.style = DZLabelStyleMake( style.backgroundColor = [UIColor greenColor]; style.cornerRedius = 3; tyle.borderColor = [UIColor redColor]; style.borderWidth = 2; style.textStyle.textColor = [UIColor darkTextColor]; style.textStyle.font = [UIFont systemFontOfSize:13]; ); 當進行賦值渲染的時候,會將Style的Copy後的實例與當前View綁定,當更改Style的屬性的時候,對應View的樣式會立刻改變。
通用樣式的共享使用原有的配置,進行通用樣式的共享是個非常困難的事情,基本上都是體力活,靠人力來維護。我們的代碼中會摻雜大量的用於配置樣式的代碼,而且是獨立且散在。 現在你可以通過StyleSheet解決: 定義共享的樣式:
//在頭文件中使用 xxx.h 聲明一個公有樣式 EXTERN_SHARE_LABEL_STYLE(Content) //在實現文件中使用 xxx.m ,實現一個公有樣式 IMP_SHARE_LABEL_STYLE(Content, style.backgroundColor = [UIColor clearColor]; style.cornerRedius = 2; style.textStyle.textColor = [UIColor redColor]; )
(1)使用共享樣式,方式一 self.label.style = DZStyleContent();
(2)使用共享樣式,方式二(推薦) 很多時候, 如果不需要進一步更改樣式,可以不采復制賦值的方式來進行渲染,可以直接使用: [DZStyleContent() decorateView:self.label]; 只進行渲染,而不進行復制。 好了,現在可以嘗試著換這種方式來寫UI樣式了。
以上就是iOS架構設計(解耦的嘗試)之UI樣式復用與布局管理的全文介紹,希望對您學習和使用ios應用開發有所幫助.【iOS架構設計(解耦的嘗試)之UI樣式復用與布局管理】的相關資料介紹到這裡,希望對您有所幫助! 提示:不會對讀者因本文所帶來的任何損失負責。如果您支持就請把本站添加至收藏夾哦!