針尖上帶著腳鐐跳舞的widget
自從iOS 10蘋果給widget做了一次大改版后,很多人都開發(fā)了自己的widget。網(wǎng)上也有很多教程,來告訴你怎么快速開發(fā)一個widget。我的這篇文章也不會重復那些簡單的創(chuàng)建extension添加證書之類的東西。我們要深入地看一下widget到底應該做成什么樣子。
你真的了解widget的尺寸嗎
首先widget由兩種狀態(tài)
- typedef NS_ENUM(NSInteger, NCWidgetDisplayMode) {
- NCWidgetDisplayModeCompact, // Fixed height
- NCWidgetDisplayModeExpanded, // Variable height
- }
大部分網(wǎng)上的教程都會告訴你,如果你想改widget的高度,都是在下面這個方法中這么寫
- - (void)widgetActiveDisplayModeDidChange:(NCWidgetDisplayMode)activeDisplayMode withMaximumSize:(CGSize)maxSize {
- if (activeDisplayMode == NCWidgetDisplayModeCompact) {
- self.preferredContentSize = CGSizeMake(maxSize.width, 110);
- } else {
- self.preferredContentSize = CGSizeMake(maxSize.width, 300);
- }
- }
這個意思就算折疊狀態(tài)110,展開狀態(tài)300。因為如果你折疊狀態(tài)就算寫120,也一樣是110的高度,這個高度不會變化。展開狀態(tài)下,當然要取比maxSize.height小的一個值。那么maxSize這個值是多少?
然而我要告訴你,高度根本就不是一個固定值!并且可以認為是無規(guī)律的!!!
因為,整個widget的maxSize限制的***規(guī)則是根據(jù)系統(tǒng)字體大小變化。
無論是折疊狀態(tài)還是展開狀態(tài)。也就是說,直接寫死110是錯誤的。因為默認系統(tǒng)字體下,的確折疊高度是110。但是一旦系統(tǒng)字體改為最小,widget折疊狀態(tài)的高度僅為95,而在系統(tǒng)字體***的情況下,widget折疊狀態(tài)的高度是140。而系統(tǒng)字體大小一共有7檔。也就是說,折疊高度和字體大小相關,但不是線性相關。
可以驗證,折疊的高度是95-100-105-110-120-135-140這七檔。且不可修改。
光折疊高度也就算了。展開***高度也是一個非線性相關的高度(并且是在折疊高度統(tǒng)一的情況下)。
以下對于展開高度的討論,都固定系統(tǒng)字體大小為默認大小,控制變量(最終得出的尺寸結果,理論上乘以7就是所有可能的高度)。
首先就是機型的差異,當然手機屏幕越小,展開***高度也就越小,這個其實尚可以接受。大不了我們按照最小屏幕的情況開發(fā)唄。然而,我要告訴你,widget***高度還是會變!
這個是我們最常見的widget入口,就是屏幕左滑的Today頁
然而其實還有另外一個入口,就是下拉通知頁的左滑,也會有入口
這兩個入口進來,widget展開狀態(tài)的***高度,后者會比前者小很多!
打斷點看maxSize很容易就可以驗證,iPhone7默認字體大小,展開狀態(tài)下。***個入口的maxSize.height是616,而另外一個情況下,這個數(shù)值變成了528。
此時真想問一聲蘋果爸爸,這到底是想搞啥?
其實還有第三個入口,就是3D touch app圖標,也會出現(xiàn)widget,但是那個只有折疊狀態(tài)
也就是說,目前來看,折疊狀態(tài)是7種尺寸,而每種屏幕大小的展開狀態(tài)下就是7*2種,也就是說,4吋,4.7吋,5.5吋這三種主流屏幕尺寸都要適配的話,展開狀態(tài)是7*2*3=42種尺寸。
看到這你可以說,沒關系,我就取4吋設備最小的高度。這個就要看你的設計師能不能同意了。
你以為完了嗎?別說iPad呢,那個咱們就不考慮了,iPhone能放下,iPad當然也放得下。
但是你真的想不到,5.5吋也就是Plus機型的橫屏狀態(tài),也是不同尺寸的。Plus橫屏下的展開模式,***個入口***高度僅有352,第二入口的***高度僅264……
意味著什么,***字體情況下的折疊狀態(tài)都有140高度,展開還不到折疊高度的兩倍。
如果你對widget的尺寸適配感興趣,并且有解決方案,請聯(lián)系我,必有重謝。
有沒有感覺被閃瞎了
你如果添加了很多個widget就會發(fā)現(xiàn),就單單在列表里上下滑動都能把眼睛閃瞎。
Widget 自身的更新機制,是進入到 Widget 后,先執(zhí)行 viewDidLoad 方法,然后是 viewWillAppear 方法。
但是經(jīng)測驗,每當某一個Widget在上下滑動,滑出屏幕后,再把這個widget劃回來,就走上面那一套刷新機制。
由于以上特性,更新代碼***寫在 viewWillAppear 方法里面,對于更新時效性特別強的,比如天氣類 app,這種***就是在該方法里面添加一個 NSTimer 定時進行刷新,在 viewWillDisAppear 方法中 進行 取消NSTimer invalidate定時更新即可。
或者,你自己實現(xiàn)緩存,一樣可以優(yōu)化。判斷如果請求來的數(shù)據(jù)和當前數(shù)據(jù)內(nèi)容一致,那么就不進行刷新列表操作。
知乎、得到 等等好多app的 Widget,只要走 viewDidLoad 方法就會閃一下,因為每次Widget加載請求的數(shù)據(jù)后會進行替換造成的。
至于為什么只要不再視線范圍再回來就刷新,我猜測,是因為內(nèi)存問題。
widget對內(nèi)存的要求之高令人發(fā)指,你的widget中一旦有gif,基本上就完全沒有辦法顯示,過一會就會顯示無法載入。不僅如此,反復來回滾動widget頁面,以不斷刷新也會導致占用內(nèi)存升高,不太清楚這個是不是蘋果的BUG,但是我自己的測試中,盡量都讓單個的widget內(nèi)存占用小于15M,這樣被殺掉內(nèi)存的機會很小。
所以,我在開發(fā)的時候,gif圖都只取***幀。并且盡可能不主動刷新UI,保持widget內(nèi)存處于一個較低的水平。
而且由于extension實際上不能直接使用主target中的那些框架,所以,我也寫了一些最基本的功能組件。
首先當然是緩存系統(tǒng),圖片緩存尤其關鍵,因為widget這種特性,會反復刷新,如果沒有緩存系統(tǒng),是非常大的浪費。首先就是圖片緩存:
- #import "QDTEImageCache.h"
- #import <CommonCrypto/CommonDigest.h>
- @implementation QDTEImageCache
- + (instancetype)shareImageCache {
- static dispatch_once_t once;
- static id instance;
- dispatch_once(&once, ^{
- instance = [self new];
- });
- return instance;
- }
- - (BOOL)isExistCacheForKey:(NSString *)key {
- key = [self cachedFileNameForKey:key];
- NSString *filePath = [[self getCachePath] stringByAppendingPathComponent:key];
- return [[NSFileManager defaultManager] fileExistsAtPath:filePath];
- }
- - (NSData *)getImageDataForKey:(NSString *)key {
- if ([self isExistCacheForKey:key]) {
- return [NSData dataWithContentsOfFile:[[self getCachePath] stringByAppendingPathComponent:[self cachedFileNameForKey:key]]];
- }
- return nil;
- }
- - (void)saveToCacheWithData:(NSData *)data forKey:(NSString *)key {
- key = [self cachedFileNameForKey:key];
- NSString *filePath = [[self getCachePath] stringByAppendingPathComponent:key];
- dispatch_async(dispatch_get_global_queue(0, 0), ^{
- [data writeToFile:filePath atomically:YES];
- });
- }
- - (NSString *)getCachePath {
- NSFileManager *fileMgr = [NSFileManager defaultManager];
- NSString *containerPath = [[fileMgr containerURLForSecurityApplicationGroupIdentifier:@"group.com.XXXXXXX"] path];
- NSString *path = [containerPath stringByAppendingString:@"/Caches/"];
- if (![fileMgr fileExistsAtPath:path]) {
- BOOL res = [fileMgr createDirectoryAtPath:path
- withIntermediateDirectories:YES
- attributes:nil
- error:nil];
- if (!res) {
- return nil;
- }
- }
- return path;
- }
- - (NSString *)cachedFileNameForKey:(NSString *)key {
- const char *str = [key UTF8String];
- if (str == NULL) {
- str = "";
- }
- unsigned char r[CC_MD5_DIGEST_LENGTH];
- CC_MD5(str, (CC_LONG)strlen(str), r);
- NSString *filename = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%@",
- r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10],
- r[11], r[12], r[13], r[14], r[15], [[key pathExtension] isEqualToString:@""] ? @"" : [NSString stringWithFormat:@".%@", [key pathExtension]]];
- return filename;
- }
- @end
一個非?;A的圖片緩存,同時配合文件管理類, 來管理接口返回的response:
控制器發(fā)出的請求,收到response的data時做一次緩存并比對
- - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
- [self.jsonData appendData:data];
- NSDictionary *dic = [[NSJSONSerialization JSONObjectWithData:self.jsonData options:NSJSONReadingMutableContainers error:nil] copy];
- if (dic == nil) return;
- self.jsonData = nil;
- NSDictionary *metaDic = [dic valueForKey:@"meta"];
- if ([[metaDic valueForKey:@"status"] integerValue] == 200) {
- NSArray *papers = [[dic valueForKey:@"response"] valueForKey:@"papers"];
- NSDictionary *paperDic = [papers firstObject];
- [_fileMgr saveToCacheWithRawDic:paperDic];
- QDTELabModel *labModle = [self modelFromRawDic:paperDic];
- if (labModle.article_id.longValue == self.labModel.article_id.longValue) return;
- self.labModel = labModle;
- dispatch_async(dispatch_get_main_queue(), ^{
- for (UIView *subView in self.view.subviews) {
- [subView removeFromSuperview];
- }
- [self refreshContentView];
- });
- }
- }
文件管理類用來儲存
- #import "QDTEFileManager.h"
- @implementation QDTEFileManager
- + (instancetype)shareManager {
- static dispatch_once_t once;
- static id instance;
- dispatch_once(&once, ^{
- instance = [self new];
- });
- return instance;
- }
- - (NSDictionary *)getUserinfo {
- NSFileManager *fileMgr = [NSFileManager defaultManager];
- NSString *containerPath = [[fileMgr containerURLForSecurityApplicationGroupIdentifier:@"group.com.XXXXXX"] path];
- NSString *filePath = [containerPath stringByAppendingPathComponent:@"QDUserinfo.json"];
- if ([fileMgr fileExistsAtPath:filePath]) {
- NSError *error;
- return [[NSJSONSerialization JSONObjectWithData:[NSData dataWithContentsOfFile:filePath] options:NSJSONReadingMutableContainers error:&error] copy];
- }
- return nil;
- }
- - (NSDictionary *)getRawDicFromCache {
- NSFileManager *fileMgr = [NSFileManager defaultManager];
- NSString *containerPath = [[fileMgr containerURLForSecurityApplicationGroupIdentifier:@"group.com.XXXXXX"] path];
- NSString *path = [containerPath stringByAppendingString:@"/Caches/"];
- NSString *filePath = [path stringByAppendingPathComponent:@"QDLabCache.json"];
- if ([fileMgr fileExistsAtPath:filePath]) {
- NSError *error;
- NSDictionary *rawDic = [[NSJSONSerialization JSONObjectWithData:[NSData dataWithContentsOfFile:filePath] options:NSJSONReadingMutableContainers error:&error] copy];
- return rawDic;
- }
- return nil;
- }
- - (void)saveToCacheWithRawDic:(NSDictionary *)rawDic {
- NSFileManager *fileMgr = [NSFileManager defaultManager];
- NSString *containerPath = [[fileMgr containerURLForSecurityApplicationGroupIdentifier:@"group.com.XXXXXX"] path];
- NSString *path = [containerPath stringByAppendingString:@"/Caches/"];
- BOOL res = [fileMgr createDirectoryAtPath:path
- withIntermediateDirectories:YES
- attributes:nil
- error:nil];
- if (!res) {
- return;
- }
- NSString *filePath = [path stringByAppendingPathComponent:@"QDLabCache.json"];
- if ([NSJSONSerialization isValidJSONObject:rawDic])
- {
- NSError *error;
- NSData *jsonData = [NSJSONSerialization dataWithJSONObject:rawDic
- options:NSJSONWritingPrettyPrinted
- error:&error];
- dispatch_async(dispatch_get_global_queue(0, 0), ^{
- [jsonData writeToFile:filePath atomically:YES];
- });
- }
- }
- - (NSString *)getServerIP
- {
- if ([self getDEBUG]) {
- NSFileManager *fileMgr = [NSFileManager defaultManager];
- NSString *containerPath = [[fileMgr containerURLForSecurityApplicationGroupIdentifier:@"group.com.XXXXXX"] path];
- NSString *filePath = [containerPath stringByAppendingPathComponent:@"QDServerIP.json"];
- if ([fileMgr fileExistsAtPath:filePath]) {
- NSError *error;
- NSArray *serverIPArr = [[NSJSONSerialization JSONObjectWithData:[NSData dataWithContentsOfFile:filePath] options:NSJSONReadingMutableContainers error:&error] copy];
- return serverIPArr.firstObject;
- }
- }
- return @"http://app3.qdaily.com";
- }
- - (BOOL)getDEBUG {
- #ifdef DEBUG
- return YES;
- #elif BETA
- return YES;
- #else
- return NO;
- #endif
- }
- @end
***呢,這個是我其中一個widget的文件結構。
widget雖小,但是我當時在開發(fā)的時候還是盡量想怎么復雜怎么做,畢竟這種東西,開發(fā)一次,幾乎以后再也不會去動了。畢竟……針尖上還要帶著腳鐐跳舞實在太累了😂。