自从iOS 10苹果给widget做了一次大改版后,很多人都开发了自己的widget。网上也有很多教程,来告诉你怎么快速开发一个widget。我的这篇文章也不会重复那些简单的创建extension添加证书之类的东西。我们要深入地看一下widget到底应该做成什么样子。
你真的了解widget的尺寸吗
首先widget由两种状态
- typedef NS_ENUM(NSInteger, NCWidgetDisplayMode) {
- NCWidgetDisplayModeCompact, // Fixed height
- NCWidgetDisplayModeExpanded, // Variable height
- }
大部分网上的教程都会告诉你,如果你想改widget的高度,都是在下面这个方法中这么写
- - (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这个值是多少?
然而我要告诉你,高度根本就不是一个固定值!并且可以认为是无规律的!!!
因为,整个widget的maxSize限制的***规则是根据系统字体大小变化。
无论是折叠状态还是展开状态。也就是说,直接写死110是错误的。因为默认系统字体下,的确折叠高度是110。但是一旦系统字体改为最小,widget折叠状态的高度仅为95,而在系统字体***的情况下,widget折叠状态的高度是140。而系统字体大小一共有7档。也就是说,折叠高度和字体大小相关,但不是线性相关。
可以验证,折叠的高度是95-100-105-110-120-135-140这七档。且不可修改。
光折叠高度也就算了。展开***高度也是一个非线性相关的高度(并且是在折叠高度统一的情况下)。
以下对于展开高度的讨论,都固定系统字体大小为默认大小,控制变量(最终得出的尺寸结果,理论上乘以7就是所有可能的高度)。
首先就是机型的差异,当然手机屏幕越小,展开***高度也就越小,这个其实尚可以接受。大不了我们按照最小屏幕的情况开发呗。然而,我要告诉你,widget***高度还是会变!
这个是我们最常见的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虽小,但是我当时在开发的时候还是尽量想怎么复杂怎么做,毕竟这种东西,开发一次,几乎以后再也不会去动了。毕竟……针尖上还要带着脚镣跳舞实在太累了😂。