针尖上带着脚镣跳舞的widget-阿里云开发者社区

开发者社区> 开发与运维> 正文
登录阅读全文

针尖上带着脚镣跳舞的widget

简介:

自从iOS 10苹果给widget做了一次大改版后,很多人都开发了自己的widget。网上也有很多教程,来告诉你怎么快速开发一个widget。我的这篇文章也不会重复那些简单的创建extension添加证书之类的东西。我们要深入地看一下widget到底应该做成什么样子。

你真的了解widget的尺寸吗

首先widget由两种状态


  1. typedef NS_ENUM(NSInteger, NCWidgetDisplayMode) { 
  2.     NCWidgetDisplayModeCompact, // Fixed height 
  3.     NCWidgetDisplayModeExpanded, // Variable height 
  4.  

大部分网上的教程都会告诉你,如果你想改widget的高度,都是在下面这个方法中这么写


  1. - (void)widgetActiveDisplayModeDidChange:(NCWidgetDisplayMode)activeDisplayMode withMaximumSize:(CGSize)maxSize { 
  2.     if (activeDisplayMode == NCWidgetDisplayModeCompact) { 
  3.         self.preferredContentSize = CGSizeMake(maxSize.width, 110); 
  4.     } else { 
  5.         self.preferredContentSize = CGSizeMake(maxSize.width, 300); 
  6.     } 
  7.  

这个意思就算折叠状态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这种特性,会反复刷新,如果没有缓存系统,是非常大的浪费。首先就是图片缓存:


  1. #import "QDTEImageCache.h" 
  2. #import <CommonCrypto/CommonDigest.h> 
  3.  
  4. @implementation QDTEImageCache 
  5.  
  6. + (instancetype)shareImageCache { 
  7.     static dispatch_once_t once; 
  8.     static id instance; 
  9.     dispatch_once(&once, ^{ 
  10.         instance = [self new]; 
  11.     }); 
  12.     return instance; 
  13.  
  14. - (BOOL)isExistCacheForKey:(NSString *)key { 
  15.     key = [self cachedFileNameForKey:key]; 
  16.     NSString *filePath = [[self getCachePath] stringByAppendingPathComponent:key]; 
  17.     return [[NSFileManager defaultManager] fileExistsAtPath:filePath]; 
  18.  
  19. - (NSData *)getImageDataForKey:(NSString *)key { 
  20.      
  21.     if ([self isExistCacheForKey:key]) { 
  22.         return [NSData dataWithContentsOfFile:[[self getCachePath] stringByAppendingPathComponent:[self cachedFileNameForKey:key]]]; 
  23.     } 
  24.     return nil; 
  25.  
  26. - (void)saveToCacheWithData:(NSData *)data forKey:(NSString *)key { 
  27.     key = [self cachedFileNameForKey:key]; 
  28.     NSString *filePath = [[self getCachePath] stringByAppendingPathComponent:key]; 
  29.      
  30.     dispatch_async(dispatch_get_global_queue(0, 0), ^{ 
  31.         [data writeToFile:filePath atomically:YES]; 
  32.     }); 
  33.      
  34.  
  35. - (NSString *)getCachePath { 
  36.     NSFileManager *fileMgr = [NSFileManager defaultManager]; 
  37.     NSString *containerPath = [[fileMgr containerURLForSecurityApplicationGroupIdentifier:@"group.com.XXXXXXX"] path]; 
  38.      
  39.     NSString *path = [containerPath stringByAppendingString:@"/Caches/"]; 
  40.     if (![fileMgr fileExistsAtPath:path]) { 
  41.         BOOL res = [fileMgr createDirectoryAtPath:path 
  42.                       withIntermediateDirectories:YES 
  43.                                        attributes:nil 
  44.                                             error:nil]; 
  45.         if (!res) { 
  46.             return nil; 
  47.         } 
  48.     } 
  49.      
  50.     return path; 
  51.  
  52. - (NSString *)cachedFileNameForKey:(NSString *)key { 
  53.     const char *str = [key UTF8String]; 
  54.     if (str == NULL) { 
  55.         str = ""
  56.     } 
  57.     unsigned char r[CC_MD5_DIGEST_LENGTH]; 
  58.     CC_MD5(str, (CC_LONG)strlen(str), r); 
  59.     NSString *filename = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%@"
  60.                           r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10], 
  61.                           r[11], r[12], r[13], r[14], r[15], [[key pathExtension] isEqualToString:@""] ? @"" : [NSString stringWithFormat:@".%@", [key pathExtension]]]; 
  62.      
  63.     return filename; 
  64. @end  

一个非常基础的图片缓存,同时配合文件管理类, 来管理接口返回的response:

控制器发出的请求,收到response的data时做一次缓存并比对


  1. - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data { 
  2.     [self.jsonData appendData:data]; 
  3.     NSDictionary *dic = [[NSJSONSerialization JSONObjectWithData:self.jsonData options:NSJSONReadingMutableContainers error:nil] copy]; 
  4.      
  5.     if (dic == nil) return
  6.      
  7.     self.jsonData = nil; 
  8.      
  9.     NSDictionary *metaDic = [dic valueForKey:@"meta"]; 
  10.      
  11.     if ([[metaDic valueForKey:@"status"] integerValue] == 200) { 
  12.          
  13.         NSArray *papers = [[dic valueForKey:@"response"] valueForKey:@"papers"]; 
  14.         NSDictionary *paperDic = [papers firstObject]; 
  15.          
  16.         [_fileMgr saveToCacheWithRawDic:paperDic]; 
  17.          
  18.         QDTELabModel *labModle = [self modelFromRawDic:paperDic]; 
  19.          
  20.         if (labModle.article_id.longValue == self.labModel.article_id.longValue) return
  21.          
  22.         self.labModel = labModle; 
  23.         dispatch_async(dispatch_get_main_queue(), ^{ 
  24.             for (UIView *subView in self.view.subviews) { 
  25.                 [subView removeFromSuperview]; 
  26.             } 
  27.             [self refreshContentView]; 
  28.         }); 
  29.     } 
  30.  

文件管理类用来储存


  1. #import "QDTEFileManager.h" 
  2.  
  3. @implementation QDTEFileManager 
  4. + (instancetype)shareManager { 
  5.     static dispatch_once_t once; 
  6.     static id instance; 
  7.     dispatch_once(&once, ^{ 
  8.         instance = [self new]; 
  9.     }); 
  10.     return instance; 
  11.  
  12. - (NSDictionary *)getUserinfo { 
  13.     NSFileManager *fileMgr = [NSFileManager defaultManager]; 
  14.     NSString *containerPath = [[fileMgr containerURLForSecurityApplicationGroupIdentifier:@"group.com.XXXXXX"] path]; 
  15.      
  16.     NSString *filePath = [containerPath stringByAppendingPathComponent:@"QDUserinfo.json"]; 
  17.     if ([fileMgr fileExistsAtPath:filePath]) { 
  18.         NSError *error; 
  19.         return [[NSJSONSerialization JSONObjectWithData:[NSData dataWithContentsOfFile:filePath] options:NSJSONReadingMutableContainers error:&error] copy]; 
  20.     } 
  21.     return nil; 
  22.  
  23. - (NSDictionary *)getRawDicFromCache { 
  24.     NSFileManager *fileMgr = [NSFileManager defaultManager]; 
  25.     NSString *containerPath = [[fileMgr containerURLForSecurityApplicationGroupIdentifier:@"group.com.XXXXXX"] path]; 
  26.     NSString *path = [containerPath stringByAppendingString:@"/Caches/"]; 
  27.     NSString *filePath = [path stringByAppendingPathComponent:@"QDLabCache.json"]; 
  28.      
  29.     if ([fileMgr fileExistsAtPath:filePath]) { 
  30.         NSError *error; 
  31.         NSDictionary *rawDic = [[NSJSONSerialization JSONObjectWithData:[NSData dataWithContentsOfFile:filePath] options:NSJSONReadingMutableContainers error:&error] copy]; 
  32.         return rawDic; 
  33.     } 
  34.     return nil; 
  35.  
  36. - (void)saveToCacheWithRawDic:(NSDictionary *)rawDic { 
  37.     NSFileManager *fileMgr = [NSFileManager defaultManager]; 
  38.     NSString *containerPath = [[fileMgr containerURLForSecurityApplicationGroupIdentifier:@"group.com.XXXXXX"] path]; 
  39.      
  40.     NSString *path = [containerPath stringByAppendingString:@"/Caches/"]; 
  41.     BOOL res = [fileMgr createDirectoryAtPath:path 
  42.                   withIntermediateDirectories:YES 
  43.                                    attributes:nil 
  44.                                         error:nil]; 
  45.     if (!res) { 
  46.         return
  47.     } 
  48.     NSString *filePath = [path stringByAppendingPathComponent:@"QDLabCache.json"]; 
  49.      
  50.     if ([NSJSONSerialization isValidJSONObject:rawDic]) 
  51.     { 
  52.         NSError *error; 
  53.         NSData *jsonData = [NSJSONSerialization dataWithJSONObject:rawDic 
  54.                                                            options:NSJSONWritingPrettyPrinted 
  55.                                                              error:&error]; 
  56.          
  57.         dispatch_async(dispatch_get_global_queue(0, 0), ^{ 
  58.             [jsonData writeToFile:filePath atomically:YES]; 
  59.         }); 
  60.     } 
  61.  
  62. - (NSString *)getServerIP 
  63.     if ([self getDEBUG]) { 
  64.         NSFileManager *fileMgr = [NSFileManager defaultManager]; 
  65.         NSString *containerPath = [[fileMgr containerURLForSecurityApplicationGroupIdentifier:@"group.com.XXXXXX"] path]; 
  66.          
  67.         NSString *filePath = [containerPath stringByAppendingPathComponent:@"QDServerIP.json"]; 
  68.          
  69.         if ([fileMgr fileExistsAtPath:filePath]) { 
  70.             NSError *error; 
  71.             NSArray *serverIPArr = [[NSJSONSerialization JSONObjectWithData:[NSData dataWithContentsOfFile:filePath] options:NSJSONReadingMutableContainers error:&error] copy]; 
  72.             return serverIPArr.firstObject; 
  73.         } 
  74.     } 
  75.     return @"http://app3.qdaily.com"
  76.  
  77. - (BOOL)getDEBUG { 
  78. #ifdef DEBUG 
  79.     return YES; 
  80. #elif BETA 
  81.     return YES; 
  82. #else 
  83.     return NO
  84. #endif 
  85. @end  

最后呢,这个是我其中一个widget的文件结构。

widget虽小,但是我当时在开发的时候还是尽量想怎么复杂怎么做,毕竟这种东西,开发一次,几乎以后再也不会去动了。毕竟……针尖上还要带着脚镣跳舞实在太累了。




作者:Curiosity
来源:51CTO

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

分享: