針尖上帶着腳鐐跳舞的widget

自從iOS 10蘋果給widget作了一次大改版後,不少人都開發了本身的widget。網上也有不少教程,來告訴你怎麼快速開發一個widget。個人這篇文章也不會重複那些簡單的建立extension添加證書之類的東西。咱們要深刻地看一下widget到底應該作成什麼樣子。json

你真的瞭解widget的尺寸嗎

首先widget由兩種狀態緩存

typedef NS_ENUM(NSInteger, NCWidgetDisplayMode) {
    NCWidgetDisplayModeCompact, // Fixed height
    NCWidgetDisplayModeExpanded, // Variable height
}

大部分網上的教程都會告訴你,若是你想改widget的高度,都是在下面這個方法中這麼寫session

- (void)widgetActiveDisplayModeDidChange:(NCWidgetDisplayMode)activeDisplayMode withMaximumSize:(CGSize)maxSize {
    if (activeDisplayMode == NCWidgetDisplayModeCompact) {
        self.preferredContentSize = CGSizeMake(maxSize.width, 110);
    } else {
        self.preferredContentSize = CGSizeMake(maxSize.width, 300);
    }
}

這個意思就算摺疊狀態110,展開狀態300。
由於若是你摺疊狀態就算寫120,也同樣是110的高度,這個高度不會變化。展開狀態下,固然要取比maxSize.height小的一個值。那麼maxSize這個值是多少?app

然而我要告訴你,高度根本就不是一個固定值!而且能夠認爲是無規律的!!!框架

由於,整個widget的maxSize限制的第一規則是根據系統字體大小變化
不管是摺疊狀態仍是展開狀態。也就是說,直接寫死110是錯誤的。由於默認系統字體下,的確摺疊高度是110。可是一旦系統字體改成最小,widget摺疊狀態的高度僅爲95,而在系統字體最大的狀況下,widget摺疊狀態的高度是140。而系統字體大小一共有7檔。也就是說,摺疊高度和字體大小相關,但不是線性相關async

能夠驗證,摺疊的高度是95-100-105-110-120-135-140這七檔。且不可修改。測試

光摺疊高度也就算了。展開最大高度也是一個非線性相關的高度(而且是在摺疊高度統一的狀況下)字體

如下對於展開高度的討論,都固定系統字體大小爲默認大小,控制變量(最終得出的尺寸結果,理論上乘以7就是全部可能的高度)。優化

首先就是機型的差別,固然手機屏幕越小,展開最大高度也就越小,這個其實尚能夠接受。大不了咱們按照最小屏幕的狀況開發唄。然而,我要告訴你,widget最大高度仍是會變!atom

這個是咱們最多見的widget入口,就是屏幕左滑的Today頁
圖片描述

然而其實還有另一個入口,就是下拉通知頁的左滑,也會有入口
圖片描述

這兩個入口進來,widget展開狀態的最大高度,後者會比前者小不少

打斷點看maxSize很容易就能夠驗證,iPhone7默認字體大小,展開狀態下。第一個入口的maxSize.height是616,而另一個狀況下,這個數值變成了528

此時真想問一聲蘋果爸爸,這究竟是想搞啥?

其實還有第三個入口,就是3D touch app圖標,也會出現widget,可是那個只有摺疊狀態
圖片描述

也就是說,目前來看,摺疊狀態是7種尺寸,而每種屏幕大小的展開狀態下就是7*2種,也就是說,4吋,4.7吋,5.5吋這三種主流屏幕尺寸都要適配的話,展開狀態是7*2*3=42種尺寸

看到這你能夠說,不要緊,我就取4吋設備最小的高度。這個就要看你的設計師能不能贊成了。
你覺得完了嗎?別說iPad呢,那個我們就不考慮了,iPhone能放下,iPad固然也放得下。
可是你真的想不到,5.5吋也就是Plus機型的橫屏狀態,也是不一樣尺寸的。Plus橫屏下的展開模式,第一個入口最大高度僅有352,第二入口的最大高度僅264……
意味着什麼,最大字體狀況下的摺疊狀態都有140高度,展開還不到摺疊高度的兩倍。

若是你對widget的尺寸適配感興趣,而且有解決方案,請聯繫我,必有重謝。

有沒有感受被閃瞎了

你若是添加了不少個widget就會發現,就單單在列表裏上下滑動都能把眼睛閃瞎。
圖片描述

Widget 自身的更新機制,是進入到 Widget 後,先執行 viewDidLoad 方法,而後是 viewWillAppear 方法。

可是經測驗,每當某一個Widget在上下滑動,滑出屏幕後,再把這個widget劃回來,就走上面那一套刷新機制。

因爲以上特性,更新代碼最好寫在 viewWillAppear 方法裏面,對於更新時效性特別強的,好比天氣類 app,這種最好就是在該方法裏面添加一個 NSTimer 定時進行刷新,在 viewWillDisAppear 方法中 進行 取消NSTimer invalidate定時更新便可。

或者,你本身實現緩存,同樣能夠優化。判斷若是請求來的數據和當前數據內容一致,那麼就不進行刷新列表操做。

知乎、獲得 等等好多app的 Widget,只要走 viewDidLoad 方法就會閃一下
,由於每次Widget加載請求的數據後會進行替換形成的。

至於爲何只要再也不視線範圍再回來就刷新,我猜想,是由於內存問題。

widget對內存的要求之高使人髮指,你的widget中一旦有gif,基本上就徹底沒有辦法顯示,過一會就會顯示沒法載入。不只如此,反覆來回滾動widget頁面,以不斷刷新也會致使佔用內存升高,不太清楚這個是否是蘋果的BUG,可是我本身的測試中,儘可能都讓單個的widget內存佔用小於15M,這樣被殺掉內存的機會很小

因此,我在開發的時候,gif圖都只取第一幀。而且儘量不主動刷新UI,保持widget內存處於一個較低的水平。

並且因爲extension實際上不能直接使用主target中的那些框架,因此,我也寫了一些最基本的功能組件。

首先固然是緩存系統,圖片緩存尤爲關鍵,由於widget這種特性,會反覆刷新,若是沒有緩存系統,是很是大的浪費。首先就是圖片緩存:

#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

一個很是基礎的圖片緩存,同時配合文件管理類, 來管理接口返回的response:

控制器發出的請求,收到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雖小,可是我當時在開發的時候仍是儘可能想怎麼複雜怎麼作,畢竟這種東西,開發一次,幾乎之後不再會去動了。畢竟……針尖上還要帶着腳鐐跳舞實在太累了?。

相關文章
相關標籤/搜索