Dancing widget with shackles on the tip of the needle

Time:2021-3-9

Since IOS 10, Apple has made a big change to the widget, many people have developed their own widget. There are also many online tutorials to show you how to quickly develop a widget. My article will not repeat the simple creation of extension, adding certificate and so on. We need to take a deeper look at what the widget should look like.

Do you really know the size of the widget

First, the widget consists of two states

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

Most of the online tutorials will tell you that if you want to change the height of the widget, it is written in the following method

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

This means folding state 110 and unfolding state 300.
Because if you fold the state, even if you write 120, it is the same height of 110, and the height will not change. In the expanded state, of course, the ratio should be taken maxSize.height A small value. What is the value of maxsize?

But I want to tell you,Height is not a fixed value at all! And can be regarded as irregular!!!

Because the maxsize of the whole widget is limitedThe first rule is to change the font size according to the system
Whether it’s folded or unfolded. That is to say, it is wrong to write 110 directly. Because in the default system font, the folding height is 110. But once the system font is minimized,The height of the widget collapse state is only 95When the system font is the largest, the height of the widget collapse state is 140. The system has a total of seven font sizes. in other words,Fold height is related to font size, but not linearly

It can be verified,The folding height is 95-100-105-110-120-135-140. It can’t be modified.

Even the light folding height.The maximum height of expansion is also a non-linear dependent height (and in the case of uniform folding height)

In the following discussion on the expansion height, the system font size is fixed as the default size, and the control variable is set(The final size result, theoretically multiplied by 7, is all possible heights)。

The first is the difference of models. Of course, the smaller the screen is, the smaller the maximum height of the mobile phone will be. This is actually acceptable. Big deal, we develop according to the smallest screen. However, I want to tell you, the maximum height of the widget will still change!

This is our most common widget entry, which is the today page on the left side of the screen
Dancing widget with shackles on the tip of the needle

However, in fact, there is another entry, that is, the left slide of the drop-down notification page
Dancing widget with shackles on the tip of the needle

These two entrances come in,The maximum height of the expanded state of the widget, which is much smaller than the former

It’s easy to verify the maxsize when you interrupt. The default font size of iPhone 7 is expanded.The first entrance maxSize.height It’s 616, and in another case, it’s 528

At this time, I really want to ask the apple dad, what is this really about?

In fact, there is a third entry, which is the 3D touch app icon. There will also be widgets, but that one will only be folded
Dancing widget with shackles on the tip of the needle

That is to say, at present,There are seven sizes in the folded state, and the expanded state of each screen size is 7 * 2. That is to say, if the three mainstream screen sizes of 4 inch, 4.7 inch and 5.5 inch need to be adapted, the expanded state is 7 * 2 * 3 = 42 sizes

When you see this, you can say, it doesn’t matter. I’ll take the minimum height of 4-inch equipment. It depends on whether your designer can agree.
Do you think it’s over? Let’s not talk about the iPad. If the iPhone can be put down, the iPad can be put down.
But you really can’t think of it, 5.5 inchesHorizontal screen status of plus modelIt’s also in different sizes.In the deployment mode under the plus horizontal screen, the maximum height of the first entrance is only 352, and the maximum height of the second entrance is only 264……
What does it mean? In the case of the largest font, the folding state has 140 height, and the unfolding is less than twice the folding height.

If you are interested in the size matching of the widget and have a solution, please contact me. Thank you very much.

Do you feel blinded

If you add a lot of widgets, you will find that just sliding up and down in the list can blind your eyes.
Dancing widget with shackles on the tip of the needle

The update mechanism of the widget itself is to execute the viewdidload method first and then the viewwillappearance method after entering the widget.

But after the test, every time a widget slides up and down, after sliding out of the screen, and then draws the widget back, it goes through the above refresh mechanism.

Because of the above characteristics, the update code is best written in the viewwillappearance method. For those apps with strong timeliness, such as weather apps, the best way is to add an nstimer in the method to refresh regularly, and cancel the nstimer invalidate update in the viewwilldisappear method.

Or, you can optimize the cache by yourself. Judge if the requested data is consistent with the current data content, then do not refresh the list operation.

Know, get, and many other app widgets, as long as the viewdidload method will flash
Because every time the widget loads the requested data, it will replace it.

As for why refresh as long as you don’t have the line of sight and come back, I guess it’s because of the memory problem.

The high memory requirement of the widget is appalling. Once there is GIF in your widget, there is basically no way to display it. After a while, it will show that it cannot be loaded. Not only that, repeatedly scrolling the widget page back and forth to refresh it continuously will also increase the memory consumption. I’m not sure if this is Apple’s bug, but in my own test,Try to make the memory consumption of a single widget less than 15m, so the chance of being killed is very small

So, when I was developing, GIF only took the first frame. And try not to refresh UI actively to keep widget memory at a low level.

Moreover, since extension can’t directly use the frameworks in the main target, I have also written some basic functional components.

First of all, of course, the cache system, especially the image cache, is the key. Because of the feature of widget, it will refresh repeatedly. If there is no cache system, it will be a great waste. The first is image caching

#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 very basic image cache, together with the file management class, to manage the response returned by the interface:

When the controller receives the response data, it caches and compares the request

- (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];
        });
    }
}

File management class is used to store

#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

Finally, this is the file structure of one of my widgets.
Dancing widget with shackles on the tip of the needle

Although the widget is small, I tried my best to figure out how to make it complicated when I was developing it. After all, this kind of thing is developed once and will never be moved again. After allIt’s tiring to dance with shackles on the tip of the needle?。