iOS上的CSS樣式協議 VKCssProtocol

折騰範兒_味精發表於2019-03-01

遷移一批老文章到掘金

CSS產生的想法

早先,寫過一陣子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

VKCssProtocol Github地址

整個專案的程式碼,以及使用demo,都在上面

這其實是一個為native開發準備的工具,是OC的程式碼,OC的實現,別被CSS的名字欺騙了╮(╯_╰)╭

對於這樣的iOS客戶端開發的場景,多少會有一定的幫助

  • UI出App設計圖有一套標準UI庫,包括大中小標題,大中小按鈕,bar配色,分割線等
  • 每種標準樣式都含有不同的底色,字號,字色,圓角,字型,陰影等等樣式屬性,屬性之間相互自有組合
  • UI有時候更換整體設計風格的時候,所有專案中用到的標準元件,應該隨著UI設計一起動態生效為最新的設計效果
  • 客戶端主題風格切換,換膚等系統

基本用法

簡單的看一個GIF吧,左邊就是CSS程式碼,後續我會給出目前已支援的CSS列表,在這裡寫完後,右側可以實時看到css效果,可以看到我準備了2個view樣式,準備了2個文字樣式,然後四個UI進行排列組合,任意交叉組合,實現各種靈活的設計

gif

先在專案裡建立.css檔案

然後在裡面寫Css程式碼,這裡我粘個樣例

.commenView1{
    background-color:orange;
    border-top: 3px solid #9AFF02;
    border-left: 5px solid 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 = [[UILabel alloc]initWithFrame:CGRectMake(20, 50, self.view.bounds.size.width - 40, 80)];
btabc.text = @"commenView1 commenText1";
[self.view addSubview:btabc];
    
UILabel *lbabc = [[UILabel alloc]initWithFrame:CGRectMake(20, 150, self.view.bounds.size.width - 40, 80)];
lbabc.text = @"commenView2 commenText1";
[self.view addSubview:lbabc];
    
UILabel *btabcd = [[UILabel alloc]initWithFrame:CGRectMake(20, 250, self.view.bounds.size.width - 40, 80)];
btabcd.text = @"commenView1 commenText2";
[self.view addSubview:btabcd];
    
UILabel *lbabcd = [[UILabel alloc]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:redView的邊框顏色,等同於layer.borderColor,冒號後是顏色引數,可以直接輸入顏色英文or #ffffff這樣的十六進位制色值

  • border-width: 2pxView的邊框寬度,等同於layer.borderWidth,冒號後是寬度引數

  • text-align: center如果含有文字,文字的左右居中對齊,等同於TextAlignment,引數可以輸入left center right justify

  • border-radius: 2pxView的邊框圓角,等同於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"

@interface VKBackgroundcolorStyle : NSObject<VKCssStyleProtocol>

@end
複製程式碼

然後在.m檔案實現裡,先使用VK_REGISTE_ATTRIBUTE()巨集向框架註冊,然後必須實現2個類方法協議

  • +styleName: 實現這個協議決定於你寫css的時候冒號前的名字
  • +setTarget: styleValue: 實現這個協議決定於你如何解讀css裡面冒號後面的引數,並且處理傳入的target,也就是目標UIView
@implementation VKBackgroundcolorStyle

VK_REGISTE_ATTRIBUTE()

+ (NSString *)styleName{
    return @"background-color";
}

+ (void)setTarget:(id)target styleValue:(id)value{
    UIColor *color = [value VKIdToColor];
    if ([target isKindOfClass:[UIView class]]) {
        [(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 = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"projectPath"];;
    NSString *cssPath = [NSString stringWithFormat:@"%@%@", 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

補充

整個結構大體如這樣,採用模組化的設計之後,就是有需求完全按著自己的意願任意擴充新支援的style屬性了。

不過有一點要補充的是

由於最近比較忙,這玩意都拖了半個月才湊合寫完,我目前已經支援的很多屬性,其實實現並不是很優雅

比如

  • border-top
  • border-bottom
  • border-right
  • border-left
  • font-weight

四邊獨立邊線湊合用比較low的方法做了,只是圖快,以後這四個模組還得再好好雕琢一下

字型加重這個模組,用的stroke結果會把字型變鏤空,反正沒啥工夫好好細弄一下

後續我還打算做 四個角不同弧度的圓角屬性

總之,這玩意還會不斷完善補充

相關文章