How to add English pronunciation?

  1. Go to phone settings and click on “Accessibility”.
  2. Click on “Spoken Content”.
  3. Click on “Voices”.
  4. Select the English language.
  5. Choose your preferred voice , click on “Download”, and wait for the download to finish.
  6. Return to MOJisho and select the downloaded voice to use.

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