iOS上的CSS樣式協議 VKCssProtocol
早先,寫過一陣子RN,前一陣子寫微信小程序,深深地覺得CSS這個東西寫起來很爽,樣式與界面完全隔離,寫好一套一套的樣式 CSS Class 然后,在寫界面HTML的時候直接對界面元素,無論是什么HTML標簽,什么控件,只要指定 CSS Class 的名字就能自動生效。
-
樣式和界面完全隔離解耦
-
樣式之間可以自由任意排列組合創建新CSS Class
-
對任意界面元素指定樣式Class名就能自動生效
客戶端場景
-
UI出App設計圖也會有一套標準UI庫
-
不同的底色,字號,字色,圓角,字體,陰影等等樣式屬性相互組合
-
UI有時候更換整體設計風格的時候,所有項目中用到的標準組件,應該隨著UI設計一起變為最新的設計效果
-
標準UI庫擴展延伸一下,就是客戶端主題風格,換膚等系統
工廠模式
看到上面的設計需求,相信很多人第一時間想到的都是客戶端里構建一套工廠模式
-
由工廠模式統一生成UI設計的標準控件
-
所有需要使用標注控件的地方都用工廠去生成
-
當UI需要整體修改樣式屬性,修改工廠模式的構造方法就能實現整體應用新效果
我不是很喜歡這種模式
-
寫法不統一,必須讓使用者使用工廠的構造方法來創建標準UI,非標準UI寫法就隨意了
-
耦合性比較強,必須引入工廠模塊
Css樣式,Protocol協議
-
希望引入Css的樣式思想,讓樣式與界面分離
-
將UI給的統一標準設計圖,專心的寫成 .css 文件
-
可以動態下發,可以動態替換,動態更新效果。
-
希望像iOS Protocol協議那樣工作
-
不管控件是Label Button Image View,CSSClass都可以直接指定一個協議名字,樣式功能就自動生效
-
不管控件是用工廠創建的,還是Xib拖線的,還是手寫代碼寫的,都能無縫接入這個cssprotocol
-
xxview.protocol = "cssclassname1 cssclassname2" 這種感覺
-
協議可以同時指定多個樣式,以空格區分,樣式之間自由組合
我不想動態修改界面元素布局,動態創建全新的界面
大家都知道,CSS有一個很大的用途就是用于界面布局,并且css的布局寫法和iOS原生的布局寫法有很大的區別,所以在這里我想強調一點,我這里寫的cssprotocol,不想包含任何跟布局有關,只是單純的動態配置外觀樣式,絲毫不影響布局。
原因是,項目里總會存在著各種各樣的布局方式,有frame布局,有masonry autolayout,也有XIB拖線autolayout,我希望我寫的東西能讓使用的人很快的在自己項目里接入,而不是一下刪掉舊的布局方案,全都替換成我的。
我希望使用者直接在現有的代碼里,無論是哪種方式實現的界面,取到UIView,直接指定cssprotocol,就自動樣式生效了,不要讓使用者需要大規模改動現有代碼。
同理,我也沒想讓這一套能夠動態的創建UI,真要動態創建原生UI,直接用samurai reactnative weex好了。
還原我的初衷,我還是希望原生開發者能在不改變自己的項目的情況下,很快的接入這個工具,對于主題樣式能夠控制的更靈活和方便。
題外話:
我就很不喜歡ASDK的設計,一整套異步渲染,flexbox頁面布局,網絡,緩存,滾動控制等等一堆完整解決方案雜糅在一起,讓使用者的代價異常的高,哪怕提供了UIKit轉ASNode的簡單入口也無法改變這一笨重無比的事實
其實ASDK每一個feature,單看源碼,單獨拆出來模塊,學習思想,吸收進入自己的項目都是很好地。
VKCssProtocol
整個項目的代碼,以及使用demo,都在上面
這其實是一個為native開發準備的工具,是OC的代碼,OC的實現,別被CSS的名字欺騙了╮(╯_╰)╭
對于這樣的iOS客戶端開發的場景,多少會有一定的幫助
- UI出App設計圖有一套標準UI庫,包括大中小標題,大中小按鈕,bar配色,分割線等
- 每種標準樣式都含有不同的底色,字號,字色,圓角,字體,陰影等等樣式屬性,屬性之間相互自有組合
- UI有時候更換整體設計風格的時候,所有項目中用到的標準組件,應該隨著UI設計一起動態生效為最新的設計效果
- 客戶端主題風格切換,換膚等系統
基本用法
簡單的看一個GIF吧,左邊就是CSS代碼,后續我會給出目前已支持的CSS列表,在這里寫完后,右側可以實時看到css效果,可以看到我準備了2個view樣式,準備了2個文字樣式,然后四個UI進行排列組合,任意交叉組合,實現各種靈活的設計
先在項目里創建.css文件
然后在里面寫Css代碼,這里我粘個樣例
.commenView1{
background-color:orange;
border-top:3pxsolid#9AFF02;
border-left:5pxsolid black;
}
.commenView2{
background-color:#FF9D6F;
border-color:black;
border-width:2px;
border-radius:15px;
}
.commenText1{
color:white ;
font-size:20px;
text-align : right;
text-transform: lowercase;
text-decoration: line-through;
}
.commenText2{
color:black ;
font-size:15px;
text-align : right;
text-transform: uppercase;
text-decoration: underline;
}
在iOS項目代碼里加載Css
在didFinishLaunch or 某個你打算加載整體Css文件的位置
//先import 頭文件
#import "VKCssProtocol.h"
//讀取bundle中名為cssDemo的css文件
@loadBundleCss(@"cssDemo");
對任意UI指定協議
UILabel*btabc = [[UILabelalloc]initWithFrame:CGRectMake(20,50,self.view.bounds.size.width -40,80)];
btabc.text = @"commenView1 commenText1";
[self.view addSubview:btabc];
UILabel*lbabc = [[UILabelalloc]initWithFrame:CGRectMake(20,150,self.view.bounds.size.width -40,80)];
lbabc.text = @"commenView2 commenText1";
[self.view addSubview:lbabc];
UILabel*btabcd = [[UILabelalloc]initWithFrame:CGRectMake(20,250,self.view.bounds.size.width -40,80)];
btabcd.text = @"commenView1 commenText2";
[self.view addSubview:btabcd];
UILabel*lbabcd = [[UILabelalloc]initWithFrame:CGRectMake(20,350,self.view.bounds.size.width -40,80)];
lbabcd.text = @"commenView2 commenText2";
[self.view addSubview:lbabcd];
上面的UI創建可以用任意方法創建,frame,autolayout,xib,隨便創建
只需要對指定的UI對象,賦值cssClass屬性,就可以指定css協議,就直接生效了,
btabc.cssClass = @"commenView1 commenText1";
lbabc.cssClass = @"commenView2 commenText1";
btabcd.cssClass = @"commenView1 commenText2";
lbabcd.cssClass = @"commenView2 commenText2";
可以對一個UI對象,指定多個cssClass協議,他們一起組合生效,優先級按最后生效的算
加載CSS的API
加載css主要依賴的是 VKCssClassManager 這個類,但提供了4個宏,可以快速方便的加載css
- VKLoadBundleCss(@"cssDemo");
加載bundle內文件名為cssDemo的.css文件
- VKLoadPathCss(@"xxx/xxx.css");
加載路徑path下的css文件
- @loadBundleCss(@"cssDemo");
等同于VKLoadBundleCss,模擬了@語法糖
- @loadPathCss(@"xxx/xxx.css");
等同于VKLoadPathCss,模擬了@語法糖
吐槽:
模擬@selector()這種的OC語法糖的方案真TM坑爹
凡是這種@loadBundleCss的宏,是無法獲得xcode提供的代碼自動補全的
直接使用VKLoadBundleCss,是可以獲得xcode代碼自動補全的
跟RAC的@strongify @weakify一樣,無法獲得代碼自動補全
這真的是一種只有裝B,沒球用的,看起來很pro的寫法
指定cssClass
上面貼過代碼,我對所有的UIView都擴寫了一個category,里面新增了一個屬性 cssClass ,對這個屬性賦值,就相當于給這個UIView對象指定所遵從的cssClass協議,可以同時指定多個cssClass協議,用空格分開。
一個cssClass其實是一系列樣式屬性style的集合,將這一系列樣式屬性組合在一起,起個名字就是cssClass了,樣給一個UI指定了cssClass就相當于一組style都生效了。
btabc.cssClass = @"commenView1 commenText1";
lbabc.cssClass = @"commenView2 commenText1";
btabcd.cssClass = @"commenView1 commenText2";
lbabcd.cssClass = @"commenView2 commenText2";
指定cssStyle
如果使用者并不打算專門寫一個cssClass,只是打算簡單的使用這個工具給一個ui賦值一個或幾個style,這也是支持的(嗯,常規的html組件也是可以寫class屬性和style屬性的嘛)
btabc.cssStyle = @"background-color:black border-color:black";
我擴寫的category里,還新增了一個屬性 cssStyle ,對這個屬性賦值,就相當于給這個UIView對象不創建一個cssClass,直接寫一個或多個style使之生效
相當于你把一個或多個style寫法,用空格分開,直接賦值給cssStyle即可
目前支持的style
-
background-color:orange; View的背景色樣式,冒號后是顏色參數,可以直接輸入顏色英文or #ffffff這樣的十六進制色值
-
color:#ffffff 如果含有文字,文字的顏色,冒號后是顏色參數,可以直接輸入顏色英文or #ffffff這樣的十六進制色值
-
font-size: 20px ; 如果含有文字,文字的字體大小,冒號后面是字號參數
-
border-color:red View的邊框顏色,等同于layer.borderColor,冒號后是顏色參數,可以直接輸入顏色英文or #ffffff這樣的十六進制色值
-
border-width: 2px View的邊框寬度,等同于layer.borderWidth,冒號后是寬度參數
-
text-align: center 如果含有文字,文字的左右居中對齊,等同于TextAlignment,參數可以輸入left center right justify
-
border-radius: 2px View的邊框圓角,等同于layer.cornerRadius,冒號后面是半徑參數
-
text: abcdefg 如果含有文字,文字的內容,后面參數是字符串
-
font-family: fontname 如果含有文字,文字的字體,等同于UIFont fontWithName的name,也可以直接輸入systemFont,boldSystemFont,italicSystemFont三個快捷輸入
-
background-image: imagenamed 如果含有image,image的名字,等同于UIImage的imageNamed的name
-
text-shadow: 2px 如果含有文字,文字的陰影寬度,后面是數字參數
-
text-transform:uppercase 如果含有文字,文字的變化,包含uppercase,lowercase,capitalize三個值,全小寫,全大寫,首字母大寫
-
text-decoration:underline 如果含有文字,文字加特殊處理,包含underline,line-through兩個值,下劃線,刪除線
-
border-top: 3px solid #9AFF02 對UIView進行上右下左的單獨邊線處理,這個值是上邊線,第一個參數是寬度,solid后面是顏色
-
border-right: 3px solid #9AFF02 對UIView進行上右下左的單獨邊線處理,這個值是右邊線,第一個參數是寬度,solid后面是顏色
-
border-bottom: 3px solid #9AFF02 對UIView進行上右下左的單獨邊線處理,這個值是下邊線,第一個參數是寬度,solid后面是顏色
-
border-left: 3px solid #9AFF02 對UIView進行上右下左的單獨邊線處理,這個值是左邊線,第一個參數是寬度,solid后面是顏色
支持靈活擴展
上面提到的每一個style都是一個模塊化組件,如果希望擴展新的style,只需要遵循并且實現模塊化協議即可輕松地在整個框架里,加入全新的style模塊
以 background-color 這個style模塊為例
隨便新建一個繼承自NSObject的類,讓這個類遵從 <VKCssStyleProtocol> 協議
#import <Foundation/Foundation.h>
#import "VKCssStylePch.h"
@interfaceVKBackgroundcolorStyle:NSObject<VKCssStyleProtocol>
@end
然后在.m文件實現里,先使用 VK_REGISTE_ATTRIBUTE() 宏向框架注冊,然后必須實現2個類方法協議
- +styleName: 實現這個協議決定于你寫css的時候冒號前的名字
- +setTarget: styleValue: 實現這個協議決定于你如何解讀css里面冒號后面的參數,并且處理傳入的target,也就是目標UIView
@implementationVKBackgroundcolorStyle
VK_REGISTE_ATTRIBUTE()
+ (NSString*)styleName{
return@"background-color";
}
+ (void)setTarget:(id)target styleValue:(id)value{
UIColor*color = [value VKIdToColor];
if([target isKindOfClass:[UIViewclass]]) {
[(UIView*)target setBackgroundColor:color];
}
}
@end
動態更新樣式
VKCssClassManager 這個類負責管理所有的css樣式表,我們希望這個css文件就好像配置表一樣,可以動態下發,這樣在未來發版之后,也能改變app的主題樣式,自然就需要一套刷新機制
+ (void)readBundleCssFile:(NSString*)cssFile;
+ (void)readCssFilePath:(NSString*)cssFilePath;
+ (void)reloadCssFile;
+ (void)clearCssFile;
上面是 VKCssClassManager 的接口,由于bundle里的css文件是不可更新的,因此刷新機制與readBundleCssFile沒啥關系,只有通過readCssFilePath路徑加載的刷新機制才有意義
- reloadCssFile 的用處就是沿著原路徑重新加載css,使用場景是新的css覆蓋了舊CSS路徑不變,在reloadCssFile的時候會自動觸發clearCssFile;
- clearCssFile 的用處是讓cssClassManager清空目前所管理的所有class;
- 在不直接使用reloadCssFile的情況下,可以先執行clearCssFile,再執行readCssFilePath,從而實現清空css后加載新路徑的css文件
HotReloader
大家在Gif里看到了像playground一樣,無需編譯和重新運行,每改一行代碼,界面就立刻實時生效的效果,主要是額外寫了一個插件 HotReloader
由于HotReloader的設計初衷是給調試,高效的實時看效果用的,因此整個HotReloader通過編譯控制,所有函數只有在模擬器編譯的情況下才有效,真機下HotReloader回自動失效
這個HotReloader不是必須的,你完全可以不使用它,整個CssProtocol一樣可以work
想要使用它需要先import頭文件 #import "VKCssHotReloader.h" ,然后在準備加載Css的地方用預編譯控制,控制模擬器下加載css的代碼變為hotReloader監聽Css
#if TARGET_IPHONE_SIMULATOR
//playground調試
//JS測試包的本地絕對路徑
NSString*rootPath = [[NSBundlemainBundle] objectForInfoDictionaryKey:@"projectPath"];;
NSString*cssPath = [NSStringstringWithFormat:@"%@%@", rootPath,@"/cssDemo.css"];
[VKCssHotReloader hotReloaderListenCssPath:cssPath];
#else
VKLoadBundleCss(@"cssDemo");
#endif
這個絕對路徑一定要填Mac的磁盤文件路徑喲,用過JSPatchPlaygroundTool的一定不會陌生
做完這件事之后還要注意2個事情
- 在你打算開啟調試的地方調用 [VKCssHotReloader startHotReloader]; (比如某個界面的ViewDidLoad)
- 在你打算停止調試的地方調用 [VKCssHotReloader endHotReloader]; (比如某個界面的dealloc)
為什么要這么做,因為一旦當你startHotReloader的時候,所有進行過cssClass,cssStyle設置的view都會被建立一個監聽,因此會造成View對象的額外持有導致的不釋放,因此當你不打算HotReload了就要關閉這個監聽endHotReloader
因為這樣的設計有可能造成使用不當的內存Leak,所以對HotReloader的所有代碼都進行了編譯控制,只有模擬器下才會工作,真機orRelease包下,無論你怎么忘記寫endHotReloader都不會造成Leak
來自:http://awhisper.github.io/2016/11/01/cssprotocol/