iOS14 Widget的使用

了解Widget
官方学习Widget Demo

Widgets显示相关的、可浏览的内容,允许用户快速访问你的App以获取更多详细信息。你的App可以提供多种小组件,让用户专注于对他们最重要的信息。

为我们的App添加新的Widget必须要明白其本身的限制:

  1. 弱交互。与之前仅可放置在负一屏的小部件相比,新版 Widget 只能借由点击固定的区域,打开主 app 内的特定页面或跳转链接。无法在不开启 app 的情况下,完成各种交互。
  2. 展示框架限定。新的 widget 仅允许使用苹果在去年(2019 年)才推出的 SwiftUI 进行布局,而不是开发者更熟悉的 UIView 框架。SwiftUI 作为一种申明式的页面布局方式,会默认的符合 苹果标准的留白和间隙,从某种角度而言自由度没有 UIView 那么高。好处是视觉统一性会更好,苹果味儿更足,坏处是会有些雷同。
  3. 刷新频率。为了省电,所有 widget 的刷新频率也是由系统统一调度,会根据用户的使用频率来调节。

根据这些我们可以知道,使用Widget,主要就是用来展示非常重要的信息,作为快捷入口等。尽量简单,不要将Widget搞成很复杂的东西。

下边主要贴代码进行一下说明,不过关于Widget的创建这里就省略了,毕竟网上多得是,例如:创建一个Widget Extension

一、布局

  1. 关于ZStack、HStack、VStack的使用。做Widget当然要先了解SwiftUI的使用,而在SwiftUI中这个三种Stack是必不可少的。可参考:您一直在等待的完整SwiftUI文档(需要科学上网),另外一个就是要学会Spacer()的使用,这个很重要。
  2. 封装。因为你的小组件可能有各种尺寸的,且可能需要多个小组件。因此尽可能的封装到每一个小的按钮,以便重复使用。
  3. 尺寸自适应。前边的限制中提到SwiftUI有自身的标准,因此可能与你的设计稿不相符,而获取到当前Widget的大小就很重要了(不仅仅只获取当前小组件的大小类型)。
  4. 黑暗模式。基本现在大家都需要适配黑暗模式吧?那么Widget也需要,而且因为SwiftUI的原因,我们可以非常简单的实现它。比如你在Widget的Assets中Add New Asset时选择Color set,就可以看到白色和黑暗模式两种颜色,按照你的需要设置色值之后,在代码中使用名字就可以了,系统会根据当前的模式自动选择颜色。而图片的使用则需要代码去判断了,以上的几条内容都可以在下边的示例代码中找到。
// 代码示例

struct OneBtnForFeaturesOneView: View {
    let btnTitle: String
    let btnUrl: String
    
    @Environment(\.colorScheme)  var colorScheme
    //view使用@ViewBuilder声明,因为它使用的view类型不同。
    @ViewBuilder
    var body: some View {
        Link(destination: URL(string: btnUrl)!){
            ZStack {
                Color("color_btn_bg")
                    .cornerRadius(16.0)
                VStack {
                    HStack(alignment: .top, content: {
                        Image(uiImage: UIImage(named: colorScheme == .dark ? "white_icon_search" : "black_icon_search")!)
                            .resizable()
                            .frame(width: 20, height: 20, alignment: .center)
                        Text(btnTitle)
                            .foregroundColor(Color("color_btn_title"))
                            .font(.system(size: 14, weight: .medium, design: .default))
                    })
                    .padding(.all, 16)
                    Spacer()
                    HStack (alignment: .bottom, content: {
                        Spacer()
                        Image(uiImage: UIImage(named: colorScheme == .dark ? "img_logo_white" : "img_logo")!)
                            .resizable()
                            .frame(width: 28, height: 28, alignment: .center)
                    })
                    .padding(.all, 16)
                }
            }
        }
    }
}


struct NewWidgetEntryView : View {
    var entry: Provider.Entry
    //针对不同尺寸的 Widget 设置不同的 View
    @Environment(\.widgetFamily) var family // 尺寸环境变量
    
    //view使用@ViewBuilder声明,因为它使用的view类型不同。
    @ViewBuilder
    var body: some View {
        // 使用 GeometryReader 获取小组件的大小
        GeometryReader{ geo in
            ZStack {
                Color("color_theme")
                    .frame(width: geo.size.width, height: geo.size.height, alignment: .center)
            }
        }
    }
}

在以上代码中,我们就可以看到与布局相关的内容

  • OneBtnForFeaturesOneView这个函数本身就是一个Button的封装,传入对应的Title和Url即可展示与跳转
  • 在这两个函数中我们可以看到ZStack、VStack的使用
  • colorScheme == .dark 即表示当前为黑暗模式,然后判断需要展示的图片
  • Color(“color_btn_title”) 即自动根据当前模式展示我们设置好的颜色
  • GeometryReader{ geo in }方法可以得到当前Widget的尺寸大小geo.size

二、数据请求

  1. 使用Swift的方式。
  2. 封装。
  3. 请求失败/默认数据的处理。在添加小组件的页面看到的样式就是加载的默认数据的样式,还有因为网络的问题请求失败的样式,将默认数据封装一下返回即可。
struct PosterData {
    static func getTodayPoster(completion: @escaping (Result<Poster, Error>) -> Void) {
        let url = URL(string: "https://nowapi.navoinfo.cn/get/now/today")!
        let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
            guard error==nil else{
                completion(.failure(error!))
                return
            }
            let poster=posterFromJson(fromData: data!)
            completion(.success(poster))
        }
        task.resume()
    }
    
    static func posterFromJson(fromData data:Data) -> Poster {
        let json = try! JSONSerialization.jsonObject(with: data, options: []) as! [String: Any]
        guard let result = json["result"] as? [String: Any] else{
            return Poster(author: "Now", content: "加载失败")
        }
        
        let author = result["author"] as! String
        let content = result["celebrated"] as! String
        let posterImage = result["poster_image"] as! String
        
        //图片同步请求
        var image: UIImage? = nil
        if let imageData = try? Data(contentsOf: URL(string: posterImage)!) {
            image = UIImage(data: imageData)
        }
        
        return Poster(author: author, content: content, posterImage: image)
    }
}

以上为一个请求示例,接口可用,类似于带图片的每日名言。来自:iOS14 Widget小组件开发(Widget Extension)

三、多个小组件以及点击跳转事件

多小组件非常的简单,其最重要的代码就是如下:

@main
struct Widgets: WidgetBundle {
    @WidgetBundleBuilder
    var body: some Widget {
        NewWidget()
        FeaturesFourWidget()
        FeaturesTwoWidget()
    }
}

添加 WidgetBundle{},然后将@main给它,在内部实现各自的一套小组件就可以了,而每套小组件都是小中大三种样式。

参考:iOS 14 小组件(2):WidgetExtension 自定义样式与交互

不过其中有一段是不对的。
iOS14 的Widget小组件可以使用AppDelegate中的 openURL 来打开。
而关键就在于,对于最小格式的systemSmall必须使用widgetURL,但是对于中大型的小组件,你的点击内容就需要包裹在Link中,记得封装就行,这样就很简单了。

主要参考的文档有:
Build Your First Widget in iOS 14 With WidgetKit(科学上网)

对了,还有个踩坑的,有问题可以看下。
iOS小组件Widget踩坑

iOS 数据导出为PDF

需求:将列表数据导出为PDF

实现方式:

一、自己创建View,按照OC的方式画页面,画完之后将一页页View绘制到PDF文件中
优点:在View中画简单易懂,转成PDF的方式也简单
缺点:由于是将整个View给当成图片绘制到PDF中,保存的PDF内都是图片,无法修改文字,且!PDF文档非常的大!

二、从头到尾都使用手绘的方式去生成PDF,将各个控件自己画出来
优点:速度很快,生成的PDF文件足够的小,例:测试我的五万条数据,基本都是纯文本的,共计3000多页,需要15s左右,文件大小只有4M,如果使用第一种方式,五千条数据都300M了,差距有点大。
缺点:整个绘制过程比较麻烦,如果是统一样式的列表,还可以用for循环,如果特殊样式太多,全都要自己写。例:一行文本数据显示一行,最后超出的部分省略号表示,这个省略号都要自己写,并且要定义样式与前边的文字相同!

实现过程:
首先定义了页面的一些常用数据

// 首先定义了页面的一些常用数据
static const CGFloat A4Width = 595.f; // PDF页面的宽
static const CGFloat A4Height = 842.f; // PDF页面的高
static const CGFloat topSpace = 40.f; // 页眉和页脚的高度
static const CGFloat bottomSpace = 50.f; // 页眉和页脚的高度 // 下边距需要留出来一定间距,不然会很挤
static const CGFloat leftRightSpace = 20.f; // 左右间距的宽度
static const CGFloat contentHeight = A4Height – topSpace – bottomSpace; // 除去页眉页脚之后的内容高度
static const CGFloat contentWidth = A4Width – leftRightSpace * 2; // 内容宽度
static const CGFloat targetSpace = 10.f; // 每个词条View的间距
static const CGFloat targetHeight = 14.f; // 词条信息每一行的高度
static const CGFloat favoritesHeight = 80.f; // 收藏夹的高度,也是收藏夹图片的高度

第一种实现方式:

/**
 通过在View上画好页面,然后绘制到PDF页面中实现转PDF, 生成的PDF文件因为内部全是图片,文件非常大
 dataInfo:MOJi数据
 pdfName: 保存的PDF名称,需要注意带上.pdf后缀!
 */
+ (void)createPDFViewWithDataInfo:(MOJiPDFDataInfo *)dataInfo PDFName:(NSString *)pdfName {
    
    NSMutableArray *viewArr = [[NSMutableArray alloc] init]; // 存放PDF的页面的数组
    
    // 存放所有词条信息View的数组
    NSMutableArray *targetViewArr = [[NSMutableArray alloc] init];
    NSMutableArray *targetHeightArr = [[NSMutableArray alloc] init]; // 存放每一个词条的所占高度
    CGFloat allTargetHeight = headerView.height + targetSpace;
    for (int i = 0; i < dataInfo.targetArr.count; i++) {
        MOJiPDFTarget *targetInfo = [dataInfo.targetArr objectAtIndex:i];

        UIView *targetView = [[UIView alloc] initWithFrame:CGRectZero];

        CGFloat height = 100.f; // 这个高度需要自己计算,此处只是示例
        
        targetView.frame = CGRectMake(0, 0, contentWidth, height);
        [targetViewArr addObject:targetView];
        [targetHeightArr addObject:@(height + targetSpace)];
        
        allTargetHeight = allTargetHeight + height + targetSpace;
    }
    
    // 补充说明,其实这里的页码计算方式是不太正确的,你需要根据自己的需求来计算
    // 计算总共需要多少页PDF
    NSInteger allPageCount = ((int)allTargetHeight % (int)contentHeight) > 0 ? (allTargetHeight / contentHeight + 1) : (allTargetHeight / contentHeight);
    
    
    int t = 0; // targetViewArr的计数放这里是为了不在PDF页码循环时重置
    for (int i = 0; i < allPageCount; i++) {
        UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, A4Width, A4Height)];
        
        // 页眉标题
        
        // 页码
        
        // 页脚
        
        CGFloat topFrom = topSpace;
        
        for (; t < targetViewArr.count; t++) {
            if (t == targetArr.count) break;

            // 剩余距离不够的情况下,翻页
            CGFloat th = [[targetHeightArr objectAtIndex:t] floatValue];
            if ((topFrom + th - targetSpace) > (A4Height - bottomSpace)) break;
            
            UIView *targetView  = [targetViewArr objectAtIndex:t];
            CGFloat targetH     = targetView.height;
            
            targetView.top      = topFrom;
            targetView.left     = leftRightSpace;
            [view addSubview:targetView];
            topFrom = topFrom + targetH + targetSpace;
        }
        
        [viewArr addObject:view];
    }
    
    // 用生成的页面生成PDF
    [MOJiPDF createPDFWithViewArr:[viewArr copy] PDFName:pdfName progress:PDFCreateProgressBlock];
}


+ (void)createPDFWithViewArr:(NSArray <UIView *>*)viewArr PDFName:(NSString *)pdfName progress:(nullable void(^)(NSString *progress))PDFCreateProgressBlock {
    
    if (viewArr.count == 0 || pdfName.length == 0) return;
    
    NSMutableData *pdfData = [NSMutableData data];
    
    // 文档信息 可设置为nil
    CFMutableDictionaryRef myDictionary = CFDictionaryCreateMutable(nil, 0,
                                             &kCFTypeDictionaryKeyCallBacks,
                                             &kCFTypeDictionaryValueCallBacks);

    CFDictionarySetValue(myDictionary, kCGPDFContextTitle, CFSTR("PDF Content Title"));
    CFDictionarySetValue(myDictionary, kCGPDFContextCreator, CFSTR("PDF Author"));
    
    // 设置PDF文件每页的尺寸
    CGRect pageRect =  CGRectMake(0, 0, A4Width, A4Height);
    // PDF绘制尺寸,设置为CGRectZero则使用默认值612*912
    UIGraphicsBeginPDFContextToData(pdfData, pageRect, nil);
    
    for (int i = 0; i < viewArr.count; i++) {
        UIView *pageView = [viewArr objectAtIndex:i];
        // PDF文档是分页的,开启一页文档开始绘制
        UIGraphicsBeginPDFPage();
        // 获取当前的上下文
        CGContextRef pdfContext = UIGraphicsGetCurrentContext();
        [pageView.layer renderInContext:pdfContext];
    }
    UIGraphicsEndPDFContext();
    
    NSArray *documentDirectories        = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentDirectory         = [documentDirectories objectAtIndex:0];
    NSString *documentDirectoryFilename = [documentDirectory stringByAppendingPathComponent:pdfName];
    [pdfData writeToFile:documentDirectoryFilename atomically:YES];
    NSLog(@"documentDirectoryFileName: %@",documentDirectoryFilename);
}

第二种实现方式:

这里有几个点非常需要注意:

  • 在PDF的页面中,默认坐标原点是在左下角的,你可能需要对页面进行坐标系转换,而且是每个页面都需要先进行坐标系转换。
  • 手动绘制时页眉页脚页码这些都需要自己绘制,特别是页码,一定要想好怎么计算总共页面数
  • 释放对象,一定要释放你Create的所有对象,不然for循环次数太多,内容太大的时候,内存会崩掉的
  • 剩下的就是,文字的字体/颜色/大小/对齐,图片的圆角等等,需要查资料看下怎么设置的问题了
/// 完全手动的画出PDF
/// @param dataInfo 需要传入的dataInfo
/// @param pdfName PDF名字,且需要带.pdf的后缀
+ (void)toDrawPDFWithDataInfo:(MOJiPDFDataInfo *)dataInfo pdfName:(nullable NSString *)pdfName  {
    
    NSArray *targetArr              = dataInfo.targetArr;
    NSMutableArray *targetHeightArr = [[NSMutableArray alloc] init]; // 存放每一个词条的所占高度

    NSInteger allPageCount = 1;
    for (int i = 0; i < targetArr.count; i++) {
        
        // 在这里写代码,计算出总共需要的页码数,以及每一个词条的高度放入targetHeightArr数组中
         
    }

    // 1.创建media box
    CGFloat myPageWidth     = A4Width;
    CGFloat myPageHeight    = A4Height;
    CGRect mediaBox         = CGRectMake (0, 0, myPageWidth, myPageHeight);

    // 2.设置pdf文档存储的路径
    NSArray *paths               = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = paths[0];
    filePath                     = [documentsDirectory stringByAppendingFormat:@"/%@", pdfName];
    const char *cfilePath        = [filePath UTF8String];
    CFStringRef pathRef          = CFStringCreateWithCString(NULL, cfilePath, kCFStringEncodingUTF8);
//    NSLog(@"filePath = %@", filePath);

    // 3.设置当前pdf页面的属性
    CFStringRef myKeys[3];
    CFTypeRef myValues[3];
    myKeys[0]   = kCGPDFContextMediaBox;
    myValues[0] = (CFTypeRef) CFDataCreate(NULL,(const UInt8 *)&mediaBox, sizeof (CGRect));
    myKeys[1]   = kCGPDFContextTitle;
    myValues[1] = CFSTR("我的PDF");
    myKeys[2]   = kCGPDFContextCreator;
    myValues[2] = CFSTR("PDF作者");

    // 4.获取pdf绘图上下文
    CGContextRef myPDFContext     = MyPDFContextCreate (&mediaBox, pathRef);

    // ————特别注意,字体样式大小和颜色要这样设置,不然无法释放——————
    // 设置字体样式
    CTFontRef ctFontTitleMedium   = CTFontCreateWithName(CFSTR("PingFangSC-Medium"), 12.0, NULL);
    // 设置字体颜色 
    CGFloat cmykValue[] = {0.239, 0.270, 0.298, 1};      
    CGColorRef ctColorBlack = CGColorCreate(CGColorSpaceCreateDeviceRGB(), cmykValue);


    int t = 0; // target的计数放这里是为了不在PDF页码循环时重置
    for (int i = 0; i < allPageCount; i++) {
        if (t == targetArr.count) break;
        
        // 5.开始描绘每一页的页面
        CFDictionaryRef pageDictionary = CFDictionaryCreate(NULL, (const void **) myKeys, (const void **) myValues, 3,
                                                            &kCFTypeDictionaryKeyCallBacks, & kCFTypeDictionaryValueCallBacks);
        CGPDFContextBeginPage(myPDFContext, pageDictionary);
        
        // 默认的原点在左下角,每一页都需要转换坐标系的操作!!!!!

        /* 添加页脚 */
        CGFloat widthFotter = [MOJiPDF getStringWidthWithFontSize:[UIFont systemFontOfSize:10.f] height:14.f string:@"这是页脚"];
        CGRect rectFooter   = CGRectMake(A4Width - 10.f - widthFotter, 10.f, widthFotter, targetHeight);
        [MOJiPDF drawTextWithText:@"这是页脚" color:ctColorBlack font:ctFontTargetRegular alignMent:kCTTextAlignmentRight rect:rectFooter maxWidth:contentWidth contextRef:myPDFContext];
        
        CGFloat topFrom = topSpace;
        for (; t < targetArr.count; t++) {
            
            // 剩余距离不够的情况下,翻页
            CGFloat th = [[targetHeightArr objectAtIndex:t] floatValue];
            if ((topFrom + th - targetSpace) > (A4Height - bottomSpace)) break;
            
            MOJiPDFTarget *targetInfo   = [targetArr objectAtIndex:t];

            if (i == 0) {
                topFrom = topSpace + favoritesHeight + targetSpace;
            
                UIImage *iconImg    = [MOJiPDF roundCorners:dataInfo.coverImg size:CGSizeMake(favoritesHeight, favoritesHeight) radius:8.f];
                CGRect iconRect     = [MOJiPDF getFinallyRectWithOriginalRect:CGRectMake(leftRightSpace, 40, favoritesHeight, favoritesHeight)];
                CGContextDrawImage(myPDFContext, iconRect, iconImg.CGImage);
                iconImg = nil;
            }
            
            CGFloat widthTargetTitle    = [MOJiPDF getStringWidthWithFontSize:[UIFont systemFontOfSize:10.f] height:14.f string:targetInfo.title];
            CGRect rectTargetTitle      = [MOJiPDF getFinallyRectWithOriginalRect:CGRectMake(leftRightSpace, topFrom, widthTargetTitle, targetHeight)];
            [MOJiPDF drawTextWithText:targetInfo.title color:ctColorBlack font:ctFontTargetMedium alignMent:kCTTextAlignmentLeft rect:rectTargetTitle maxWidth:contentWidth contextRef:myPDFContext];
            topFrom = topFrom + targetHeight;
        }
        
        CGPDFContextEndPage(myPDFContext);
        CFRelease(pageDictionary);
    }

    // 6.释放创建的对象
    CFRelease(ctColorBlack);
    CFRelease(ctFontTitleMedium);
    
    CGContextRelease(myPDFContext);
    CFRelease(myValues[0]);
    CFRelease(myValues[1]);
    CFRelease(myValues[2]);
    CFRelease(myKeys[0]);
    CFRelease(myKeys[1]);
    CFRelease(myKeys[2]);
    CFRelease(pathRef);
}

以上为主要的代码实现,手动绘制,如何绘制图片/文字等比较麻烦,不过基本都可以在网上找到,至于每个函数的意义,可以参考苹果的官方文档及以下示例:

/*
 * 获取pdf绘图上下文
 * inMediaBox指定pdf页面大小
 * path指定pdf文件保存的路径
 */
CGContextRef MyPDFContextCreate (const CGRect *inMediaBox, CFStringRef path)
{
    CGContextRef myOutContext = NULL;
    CFURLRef url;
    CGDataConsumerRef dataConsumer;
    
    url = CFURLCreateWithFileSystemPath (NULL, path, kCFURLPOSIXPathStyle, false);
    
    if (url != NULL)
    {
        dataConsumer = CGDataConsumerCreateWithURL(url);
        if (dataConsumer != NULL)
        {
            myOutContext = CGPDFContextCreate (dataConsumer, inMediaBox, NULL);
            CGDataConsumerRelease (dataConsumer);
        }
        CFRelease(url);
    }
    return myOutContext;
}


/**
 绘制文字的方式
 text: 需要绘制的文字
 color:文字颜色
 font:文字字体及大小
 alignment:文字对齐方式 (注:这个参数在原先的写法中没有生效,不知道为什么,暂时不用管它)
 rect:文字所在范围
 maxWidth:最大显示宽度,大于此,先截取然后显示省略
 contextRef:上下文
 */
+ (void)drawTextWithText:(NSString *)text color:(CGColorRef)color font:(CTFontRef)font alignMent:(CTTextAlignment)alignment rect:(CGRect)rect maxWidth:(CGFloat)maxWidth contextRef:(CGContextRef)contextRef {
    
    CFStringRef keys[]      = {kCTFontAttributeName, kCTForegroundColorAttributeName};
    CFTypeRef values[]      = {font, color};
    CFDictionaryRef attr    = CFDictionaryCreate(NULL, (const void **)&keys, (const void **)&values, 2, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
    
    CFAttributedStringRef attrString   = CFAttributedStringCreate(NULL, (__bridge CFStringRef)text, attr);
    CTLineRef line          = CTLineCreateWithAttributedString(attrString);
    
    NSString *dotString     = @"\u2026";
    CFAttributedStringRef dotStringRef = CFAttributedStringCreate(NULL, (__bridge CFStringRef)dotString, attr);
    CTLineRef token         = CTLineCreateWithAttributedString(dotStringRef);
    
    /** 将现有 CTLineRef 截断并返回一个新的对象
     * width 截断宽度:如果行宽大于截断宽度,则该行将被截断
     * truncationType 截断类型
     * truncationToken 截断用的填充符号,通常是省略号 ... ,为Null时则只截断,不做填充
     *                        该填充符号的宽度必须小于截断宽度,否则该函数返回 NULL;
     */
    CTLineRef newline = CTLineCreateTruncatedLine(line, maxWidth, kCTLineTruncationEnd, token);
    
    CGContextSetTextMatrix(contextRef, CGAffineTransformIdentity);
    CGContextSetTextPosition(contextRef, rect.origin.x, rect.origin.y);
    CTLineDraw(newline, contextRef);
    
    CFRelease(newline);
    CFRelease(token);
    CFRelease(line);
    CFRelease(dotStringRef);
    CFRelease(attrString);
    CFRelease(attr);
    
    CFRelease(keys[0]);
    CFRelease(keys[1]);
}


// 获取字符串宽度
+ (CGFloat)getStringWidthWithFontSize:(UIFont *)sizeFont height:(CGFloat)height string:(NSString *)string {
    
    CGRect rect = [string boundingRectWithSize:CGSizeMake(MAXFLOAT, height) options:NSStringDrawingTruncatesLastVisibleLine | NSStringDrawingUsesFontLeading | NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName:sizeFont} context:nil];
    return rect.size.width;
}


// 根据正确的坐标系 转换为在PDF画布上的坐标系
+ (CGRect)getFinallyRectWithOriginalRect:(CGRect)originalRect {
    
    CGFloat y = A4Height - originalRect.origin.y - originalRect.size.height;
    return CGRectMake(originalRect.origin.x, y, originalRect.size.width, originalRect.size.height);
}


/**
 给UIImage添加圆角
 img: 需要处理的UIImage
 size:UIImage真实显示时候的size
 radius:UIImage真实显示时候的圆角大小
 */
+ (UIImage *)roundCorners:(UIImage*)img size:(CGSize)size radius:(CGFloat)radius {
    
    int w = img.size.width;
    int h = img.size.height;
    CGFloat modulus = w / size.width; // 本身画图,是根据img的原始尺寸来的,跟要展示的尺寸会不同,需要自己计算在原尺寸上的圆角大小
   
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    /**
     CGContextRef CGBitmapContextCreate (

        void *data,                 指向要渲染的绘制内存的地址。这个内存块的大小至少是(bytesPerRow*height)个字节
        size_t width,               bitmap的宽度,单位为像素
        size_t height,              bitmap的高度,单位为像素
        size_t bitsPerComponent,    内存中像素的每个组件的位数.例如,对于32位像素格式和RGB 颜色空间,你应该将这个值设为8.
        size_t bytesPerRow,         bitmap的每一行在内存所占的比特数
        CGColorSpaceRef colorspace, bitmap上下文使用的颜色空间。
        CGBitmapInfo bitmapInfo     指定bitmap是否包含alpha通道,像素中alpha通道的相对位置,像素组件是整形还是浮点型等信息的字符串。
     ); */
    CGContextRef context = CGBitmapContextCreate(NULL, w, h, 8, 8 * w, colorSpace, kCGImageAlphaPremultipliedFirst);
   
    CGContextBeginPath(context);
    addRoundedRectToPath(context, CGRectMake(0, 0, w, h), radius * modulus, radius * modulus);
    CGContextClosePath(context);
    CGContextClip(context);
   
    CGContextDrawImage(context, CGRectMake(0, 0, w, h), img.CGImage);
   
    CGImageRef imageMasked = CGBitmapContextCreateImage(context);
    CGContextRelease(context);
    CGColorSpaceRelease(colorSpace);
    
    UIImage * image = [UIImage imageWithCGImage:imageMasked];
    CGImageRelease(imageMasked);
   
    return image;
}


//这是被调用的静态方法,绘制圆角用
static void addRoundedRectToPath(CGContextRef context, CGRect rect,
                                float ovalWidth,float ovalHeight)
{
   float fw, fh;
   if (ovalWidth == 0 || ovalHeight == 0) {
       CGContextAddRect(context, rect);
       return;
   }
   
   CGContextSaveGState(context);
   CGContextTranslateCTM (context, CGRectGetMinX(rect), CGRectGetMinY(rect));
   CGContextScaleCTM (context, ovalWidth, ovalHeight);
   fw = CGRectGetWidth (rect) / ovalWidth;
   fh = CGRectGetHeight (rect) / ovalHeight;
   CGContextMoveToPoint(context, fw, fh/2);
   CGContextAddArcToPoint(context, fw, fh, fw/2, fh, 1);
   CGContextAddArcToPoint(context, 0, fh, 0, fh/2, 1);
   CGContextAddArcToPoint(context, 0, 0, fw/2, 0, 1);
   CGContextAddArcToPoint(context, fw, 0, fw, fh/2, 1);
   CGContextClosePath(context);
   CGContextRestoreGState(context);
}

不习惯本页面的,可以查看此页面:https://www.jianshu.com/p/cb88ea5e750a