iOS development – download CGFontRef using network special fonts

iOS Development – Download CoreText Using Network Special Fonts

During the development, when the font needs to be downloaded and then displayed, this special font can only be normal after downloading.

1. Font Downloader

Add in AFNetworking

pod 'Reachability'

The font downloader uses AFNetworking to download font files
code show as below

#import "SDFontDownloaderClient.h"
#import "AFNetworking.h"

@implementation SDFontDownloaderClientError

@end

@interface SDFontDownloaderClient()

@property (nonatomic, strong) AFHTTPSessionManager *httpManager;

@end

@implementation SDFontDownloaderClient

 + (instancetype)sharedInstance {<!-- -->
    static SDFontDownloaderClient *_sharedInstance = nil;
    static dispatch_once_t onceToken;
    dispatch_once( & amp;onceToken, ^{<!-- -->
        _sharedInstance = [[SDFontDownloaderClient alloc] init];
        _sharedInstance.httpManager = [AFHTTPSessionManager manager];
        _sharedInstance.httpManager.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@"application/json", @"text/json", @"text/javascript", @"text/html", @"text/plain", nil];
    });
    
    return_sharedInstance;
}

- (AFHTTPSessionManager *)httpManager{<!-- -->
    if (!_httpManager) {<!-- -->
        _httpManager = [[AFHTTPSessionManager alloc] init];
        _httpManager.operationQueue.maxConcurrentOperationCount = 6;
        _httpManager.requestSerializer = [AFJSONRequestSerializer serializer];
        [_httpManager.requestSerializer willChangeValueForKey:@"timeoutInterval"];
        [_httpManager.requestSerializer setTimeoutInterval:10];
        [_httpManager.requestSerializer setStringEncoding:NSUTF8StringEncoding];
        _httpManager.responseSerializer = [AFJSONResponseSerializer serializer];

        _httpManager.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@"text/plain", @"multipart/form-data", @"application/json", @"text/html", @"image/jpeg", @"image /png", @"application/octet-stream", @"text/json", @"text/javascript", @"text/html", nil];
        
        _httpManager.requestSerializer.HTTPMethodsEncodingParametersInURI = [NSSet setWithArray:@[@"POST", @"GET", @"HEAD", @"PUT", @"DELETE"]];
    }
    
    [_httpManager.requestSerializer setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];

    return _httpManager;
}

#pragma mark - Http Request Failure
- (SDFontDownloaderClientError *)httpRequestFailure:(NSHTTPURLResponse *)response
                            error:(NSError *)error {<!-- -->
    
    SDFontDownloaderClientError *e = [[SDFontDownloaderClientError alloc] init];
    if(error.code == NSURLErrorNotConnectedToInternet || error.code == NSURLErrorCannotFindHost || error.code == NSURLErrorCannotConnectToHost){<!-- -->
        e.message = @"Network connection failed!";
        return e;
    }
    
    if (error.code == NSURLErrorTimedOut){<!-- -->
        e.message = @"Network connection timed out!";
        return e;
    }
    
    NSInteger statusCode = response. statusCode;
    if (statusCode == 401) {<!-- -->
        e.message = @"Authentication failed";
    } else if (statusCode == 400){<!-- -->
        e.message = @"Invalid request";
    } else if (statusCode == 404) {<!-- -->
        e.message = @"The accessed resource is lost!";
    } else if (statusCode >= 500){<!-- -->
        e.message = @"The server is exhausted!";
    }
    
    #ifdef DEBUG
        @try {<!-- -->
            // here is just for testing
            //The first step, first get the value from the error according to NSErrorFailingURLKey
            NSError *errorFail = [error. userInfo objectForKey:@"NSUnderlyingError"];
            //The second step, get the value according to com.alamofire.serialization.response.error.data through errorFail
            NSData *data = nil;
            if (errorFail) {<!-- -->
                data = [errorFail.userInfo objectForKey:@"com.alamofire.serialization.response.error.data"];
            } else {<!-- -->
                data = [error.userInfo objectForKey:@"com.alamofire.serialization.response.error.data"];
            }
            NSLog(@"data:%@",data);
            //The third part, convert NSData to NSString, because NSString string is more intuitive
            if (data) {<!-- -->
                NSString *errorString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
                NSLog(@"errorString:%@",errorString);
            }
            
        } @catch (NSException *exception) {<!-- -->
            
        } @finally {<!-- -->
            
        }
                
    #else
    #endif
    
    return e;
}


#pragma mark - Http download
/**
 request download
 
 @param aUrl aurl
 @param aSavePath aSavePath
 @param aFileName aFileName
 @param aTag aTag
 @param downloadprogress downloadprogress
 @param success success
 @param failure failure
 */
- (void)downloadFileURL:(NSString *)aUrl
               savePath:(NSString *)aSavePath
               fileName:(NSString *)aFileName
                    tag:(NSInteger)aTag
       downloadProgress:(void(^)(CGFloat progress))downloadprogress
                success:(void(^)(NSURLResponse *response,NSString *filePath))success
                failure:(void(^)(SDFontDownloaderClientError * e))failure {<!-- -->
    
    NSFileManager *fileManger = [NSFileManager defaultManager];
    
    if ([fileManger fileExistsAtPath:[aSavePath stringByAppendingPathComponent:aFileName]]) {<!-- -->
        // file exists
        return;
    }
    
    //2. Determine the URL address of the request
    NSString *requestUrl = aUrl;

    NSMutableURLRequest *request = [self.httpManager.requestSerializer requestWithMethod:@"GET" URLString:requestUrl parameters:nil error:nil];
    
    __block NSURLSessionDownloadTask *downloadTask = nil;
    
    downloadTask = [self.httpManager downloadTaskWithRequest:request progress:^(NSProgress * _Nonnull downloadProgress) {<!-- -->
        NSLog(@"progress current thread:%@", [NSThread currentThread]);
        dispatch_async(dispatch_get_main_queue(), ^{<!-- -->
            
            downloadprogress(1.0 * downloadProgress.completedUnitCount / downloadProgress.totalUnitCount);
        });
        
    } destination:^NSURL * _Nonnull(NSURL * _Nonnull targetPath, NSURLResponse * _Nonnull response) {<!-- -->
        NSLog(@"destination current thread:%@", [NSThread currentThread]);
        return [NSURL fileURLWithPath:aSavePath];
        
    } completionHandler:^(NSURLResponse * _Nonnull response, NSURL * _Nullable filePath, NSError * _Nullable error) {<!-- -->
        NSLog(@"completionHandler current thread:%@", [NSThread currentThread]);
        if(error == nil) {<!-- -->
            success(response,[filePath path]);
        } else {<!-- -->
            //download failed
            NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
            SDFontDownloaderClientError *e = [self httpRequestFailure:httpResponse error:error];
            failure(e);
        }
    }];
    
    [downloadTask resume];
}

@end

2. Font management

After the font file is downloaded, it will be registered to CTFontManager

//Register the font file under the specified path
- (void)registerFont:(NSString *)fontPath {<!-- -->
    //Adjust position
    NSURL *fontUrl = [NSURL fileURLWithPath:fontPath];
    CGDataProviderRef providerRef = CGDataProviderCreateWithURL((__bridge CFURLRef)fontUrl);
    
    CFErrorRef error;
    CGFontRef font = CGFontCreateWithDataProvider(providerRef);
    if(!font){<!-- -->
        CGDataProviderRelease(providerRef);
        CGFontRelease(font);
        return;
    }

    BOOL ctfmrgf = CTFontManagerRegisterGraphicsFont(font, &error);
    if (!ctfmrgf) {<!-- -->
        //registration failed
        CFStringRef errorDescription = CFErrorCopyDescription(error);
        CFRelease(errorDescription);
        if (error) {<!-- -->
            CFRelease(error);
        }
    }

    CGFontRelease(font);
    CFRelease(providerRef);
}

The complete code for font management is as follows

#import "SDFontManager.h"

@implementation SDFontLoadError

@end


static SDFontManager *manager = nil;

@interface SDFontManager()

@end

@implementation SDFontManager

 + (instancetype)shareInstance {
    static dispatch_once_t onceToken;
    dispatch_once( & amp;onceToken, ^{
        manager = [[SDFontManager alloc] init];
    });
    return manager;
}

- (void)downloadAppleFontName:(NSString *)fontName
                     fontSize: (CGFloat) fontSize
               beginLoadBlock:(SDFontBeginLoadBlock)beginLoadBlock
                progressBlock: (SDFontLoadProgressBlock) progressBlock
              completionBlock:(SDFontLoadCompletionBlock)completionBlock {
    
    self.beginLoadBlock = beginLoadBlock;
    self.progressBlock = progressBlock;
    self. completionBlock = completionBlock;
    
    UIFont* aFont = [UIFont fontWithName:fontName size:fontSize];
    // If the font is already downloaded
    if (aFont & amp; & amp; ([aFont.fontName compare:fontName] == NSOrderedSame || [aFont.familyName compare:fontName] == NSOrderedSame)) {
        // Go ahead and display the sample text.
        if (self.beginLoadBlock) {
            self.beginLoadBlock();
        }
        
        if (self. progressBlock) {
            self.progressBlock(1.0);
        }
        
        if (self. completionBlock) {
            self. completionBlock(aFont, nil);
        }
        return;
    }
    
    // Create a dictionary with the font's PostScript name.
    NSMutableDictionary *attrs = [NSMutableDictionary dictionaryWithObjectsAndKeys:fontName, kCTFontNameAttribute, nil];
    
    // Create a new font descriptor reference from the attributes dictionary.
    CTFontDescriptorRef desc = CTFontDescriptorCreateWithAttributes((__bridge CFDictionaryRef)attrs);
    
    NSMutableArray *descs = [NSMutableArray arrayWithCapacity:0];
    [descs addObject:(__bridge id)desc];
    CFRelease(desc);
    
    __block BOOL errorDuringDownload = NO;
    
    // Start processing the font descriptor..
    // This function returns immediately, but can potentially take long time to process.
    // The progress is notified via the callback block of CTFontDescriptorProgressHandler type.
    // See CTFontDescriptor.h for the list of progress states and keys for progressParameter dictionary.
    CTFontDescriptorMatchFontDescriptorsWithProgressHandler( (__bridge CFArrayRef)descs, NULL, ^(CTFontDescriptorMatchingState state, CFDictionaryRef progressParameter) {
        
        //NSLog( @"state %d - %@", state, progressParameter);
        
        double progressValue = [[(__bridge NSDictionary *)progressParameter objectForKey:(id)kCTFontDescriptorMatchingPercentage] doubleValue];
        
        if (state == kCTFontDescriptorMatchingDidBegin) {
            dispatch_async( dispatch_get_main_queue(), ^ {
                // Show an activity indicator
                // Start downloading fonts and display the loading progress
                if (self.beginLoadBlock) {
                    self.beginLoadBlock();
                }
                NSLog(@"Begin Matching");
            });
        } else if (state == kCTFontDescriptorMatchingDidFinish) {
            dispatch_async( dispatch_get_main_queue(), ^ {
                // Remove the activity indicator
                if (!errorDuringDownload) {
                    NSLog(@"%@ downloaded", fontName);
                }
                
                if(self.progressBlock){
                    self.progressBlock(1.0f);
                }
                
                // Finish downloading fonts
                if (self. completionBlock) {
                    if ([self isAvaliableFont:fontName fontSize:fontSize]) {
                        [self saveAppleFontPathWithFontName:fontName];
                        UIFont *aFont = [UIFont fontWithName:fontName size:fontSize];
                        self. completionBlock(aFont, nil);
                    } else {
                        NSLog(@"font %@ is Unavaliable", fontName);
                    }
                }
            });
        } else if (state == kCTFontDescriptorMatchingWillBeginDownloading) {
            dispatch_async( dispatch_get_main_queue(), ^ {
                // Show a progress bar
                if(self.progressBlock){
                    self.progressBlock(0.0f);
                }
                NSLog(@"Begin Downloading");
            });
        } else if (state == kCTFontDescriptorMatchingDidFinishDownloading) {
            dispatch_async( dispatch_get_main_queue(), ^ {
                // Remove the progress bar
                if(self.progressBlock){
                    self.progressBlock(1.0f);
                }
                NSLog(@"Finish downloading");
            });
        } else if (state == kCTFontDescriptorMatchingDownloading) {
            dispatch_async( dispatch_get_main_queue(), ^ {
                // Use the progress bar to indicate the progress of the downloading
                if(self.progressBlock){
                    self.progressBlock(progressValue / 100.0);
                }
                NSLog(@"Downloading %.0f%% complete", progressValue);
            });
        } else if (state == kCTFontDescriptorMatchingDidFailWithError) {
            // An error has occurred.
            // Get the error message
            NSError *error = [(__bridge NSDictionary *)progressParameter objectForKey:(id)kCTFontDescriptorMatchingError];
            NSString *errorMessage = nil;
            if (error != nil) {
                errorMessage = [error description];
            } else {
                errorMessage = @"ERROR MESSAGE IS NOT AVAILABLE!";
            }
            // Set our flag
            errorDuringDownload = YES;
            
            dispatch_async( dispatch_get_main_queue(), ^ {
                if (self. completionBlock) {
                    SDFontLoadError *error = [[SDFontLoadError alloc] init];
                    error. errorMessage = errorMessage;
                    self. completionBlock(nil, error);
                }
                NSLog(@"Download error: %@", errorMessage);
            });
        }
        
        return (bool) YES;
    });
}

- (void)downloadCustomFontName:(NSString *)fontName
               fontDownloadUrl:(NSString *)fontDownloadUrl
                      fontSize: (CGFloat) fontSize
                beginLoadBlock:(SDFontBeginLoadBlock)beginLoadBlock
                 progressBlock: (SDFontLoadProgressBlock) progressBlock
               completionBlock:(SDFontLoadCompletionBlock)completionBlock {
    
    self.beginLoadBlock = beginLoadBlock;
    self.progressBlock = progressBlock;
    self. completionBlock = completionBlock;
    
    UIFont* aFont = [UIFont fontWithName:fontName size:fontSize];
    // If the font is already downloaded
    if (aFont & amp; & amp; ([aFont.fontName compare:fontName] == NSOrderedSame || [aFont.familyName compare:fontName] == NSOrderedSame)) {
        // Go ahead and display the sample text.
        if (self.beginLoadBlock) {
            self.beginLoadBlock();
        }
        
        if (self. progressBlock) {
            self.progressBlock(1.0);
        }
        
        if (self. completionBlock) {
            self. completionBlock(aFont, nil);
        }
        return;
    }
    
    //If it does not exist, re-download and decompress
    NSString *savefontDirectoryPath = [self fontDirectoryPath];

    //download successful
    NSString *afileName = [[NSURL URLWithString:fontDownloadUrl] lastPathComponent];
    NSString *afilePath = [NSString pathWithComponents:@[savefontDirectoryPath, afileName]];
    
    //download successful
    NSString *aFontFileName = [[NSURL URLWithString:fontDownloadUrl] lastPathComponent];

    BOOL exsit = [self exsitCustomFontFileWithFontName:aFontFileName];
    if (exsit) { //if already downloaded
        // check if the font is available
        [self registerFont:afilePath];
        UIFont *font = [self fontWithPath:afilePath fontSize:fontSize];

        //Update the UI
        if (self. progressBlock) {
            self.progressBlock(1.0);
        }
        
        if (self. completionBlock) {
            self. completionBlock(font, nil);
        }
        
        return;
    }
    
    __weak typeof(self) weakSelf = self;
    [[SDFontDownloaderClient sharedInstance] downloadFileURL:fontDownloadUrl savePath:afilePath fileName:afilePath tag:[afilePath hash] downloadProgress:^(CGFloat progress) {
        if (self. progressBlock) {
            self.progressBlock(progress);
        }
    } success:^(NSURLResponse *response, NSString *filePath) {
        NSLog(@"filePath:%@",filePath);
        dispatch_async(dispatch_get_main_queue(), ^{
            NSString *fontPath = filePath;
            [weakSelf registerFont:fontPath]; //register font file
            UIFont *font = [weakSelf fontWithPath:fontPath fontSize:fontSize];
            if (weakSelf. completionBlock) {
                weakSelf. completionBlock(font, nil);
            }
        });
    } failure:^(SDFontDownloaderClientError *e) {
        dispatch_async(dispatch_get_main_queue(), ^{
            SDFontLoadError *error = [[SDFontLoadError alloc] init];
            error.errorMessage = e.message;
            if (weakSelf. completionBlock) {
                weakSelf. completionBlock(nil, error);
            }
        });
        
    }];
}

//Register Apple fonts and save the path (here Apple's fonts are not in the sandbox directory and cannot be registered)
- (void)saveAppleFontPathWithFontName:(NSString *)fontName{

    CTFontRef fontRef = CTFontCreateWithName((__bridge CFStringRef)fontName, 0., NULL);
    CFURLRef fontURL = CTFontCopyAttribute(fontRef, kCTFontURLAttribute);
    NSURL *fontPathURL = (__bridge NSURL*)(fontURL);

    //Save Apple's font path
    [self registerFont:fontPathURL.path]; //register font

    CFRelease(fontURL);
    CFRelease(fontRef);
}

//Register the font file under the specified path
- (void)registerFont:(NSString *)fontPath {<!-- -->
    //Adjust position
    NSURL *fontUrl = [NSURL fileURLWithPath:fontPath];
    CGDataProviderRef providerRef = CGDataProviderCreateWithURL((__bridge CFURLRef)fontUrl);
    
    CFErrorRef error;
    CGFontRef font = CGFontCreateWithDataProvider(providerRef);
    if(!font){<!-- -->
        CGDataProviderRelease(providerRef);
        CGFontRelease(font);
        return;
    }

    BOOL ctfmrgf = CTFontManagerRegisterGraphicsFont(font, &error);
    if (!ctfmrgf) {<!-- -->
        //registration failed
        CFStringRef errorDescription = CFErrorCopyDescription(error);
        CFRelease(errorDescription);
        if (error) {<!-- -->
            CFRelease(error);
        }
    }

    CGFontRelease(font);
    CFRelease(providerRef);
}

- (UIFont *)fontWithPath:(NSString *)fontPath fontSize:(CGFloat)fontSize {
    NSURL *fontUrl = [NSURL fileURLWithPath:fontPath];
    CGDataProviderRef providerRef = CGDataProviderCreateWithURL((__bridge CFURLRef)fontUrl);
    CGFontRef font = CGFontCreateWithDataProvider(providerRef);
    if(!font){
        CGDataProviderRelease(providerRef);
        CGFontRelease(font);
        return nil;
    }
    CGDataProviderRelease(providerRef);
    CTFontManagerUnregisterGraphicsFont(font, nil);
    CTFontManagerRegisterGraphicsFont(font, NULL);
    
    NSString *newFamilyName = CFBridgingRelease(CGFontCopyPostScriptName(font));
    UIFont *uifont = [UIFont fontWithDescriptor:[UIFontDescriptor fontDescriptorWithName:newFamilyName size:fontSize] size:fontSize];

    CGFontRelease(font);
    return uifont;
}

/ / Determine whether the font is available
- (BOOL)isAvaliableFont:(NSString *)fontName fontSize:(CGFloat)fontSize {
    UIFont *aFont = [UIFont fontWithName:fontName size:fontSize];
    return aFont & amp; & amp; ([aFont.fontName compare:fontName] == NSOrderedSame || [aFont.familyName compare:fontName] == NSOrderedSame);
}

//Whether fontFileName exists
- (BOOL)exsitCustomFontFileWithFontName:(NSString *)fontName {
    NSString *fontPath = [[self fontDirectoryPath] stringByAppendingPathComponent:fontName];
    BOOL exit = [[NSFileManager defaultManager] fileExistsAtPath:fontPath];
    return exit;
}

// The path path where the font is stored
- (NSString *)fontDirectoryPath {
    NSString *documentsDirectory = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
    NSString *fontDirectoryPath = [documentsDirectory stringByAppendingPathComponent:@"fonts"];

    [self createDirectoryIfNotExsitPath:fontDirectoryPath]; //create directory

    return fontDirectoryPath;
}

//Create a directory
- (BOOL)createDirectoryIfNotExsitPath:(NSString *)path {
    BOOL success = YES;
    if(![[NSFileManager defaultManager] fileExistsAtPath:path]){ //if create folder
        NSError * error = nil;
        success = [[NSFileManager defaultManager] createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error: &error];
        if (!success || error) {
            NSLog(@"Error! %@", error);
        } else {
            NSLog(@"Create fonts directory Success!");
        }
    }
    return success;
}

//Construct font according to fontName
- (UIFont *)fontWithFontName:(NSString *)fontName fontSize:(CGFloat)fontSize{
    if ([self isAvaliableFont:fontName fontSize:fontSize]) {
        return [UIFont fontWithName:fontName size:fontSize];
    }
    return nil;
}

@end

3. Use downloaded and registered fonts

I use downloaded and registered fonts here

- (void)lookButtonAction {<!-- -->
    [[SDFontManager shareInstance] downloadCustomFontName:@"猫吃网糖糖系体" fontDownloadUrl:@"https://j1-common-bucket.s3.cn-northwest-1.amazonaws.com.cn/as/2020/12/ 16/oLmJQK1608104260841.ttf" fontSize:20 beginLoadBlock:^{<!-- -->
        NSLog(@"beginLoadBlock");
    } progressBlock:^(CGFloat progress) {<!-- -->
        NSLog(@"progressBlock:%f", progress);
    } completionBlock:^(UIFont *font, SDFontLoadError *error) {<!-- -->
        NSLog(@"completionBlock font:%@, error:%@", font, error);
        if (font & amp; & amp; !error) {<!-- -->
            self.titleLabel.font = font;
        }
    }];
}

#pragma mark - lazy
- (UILabel *)titleLabel {<!-- -->
    if (!_titleLabel) {<!-- -->
        _titleLabel = [[UILabel alloc] initWithFrame:CGRectZero];
        _titleLabel.backgroundColor = [UIColor clearColor];
        _titleLabel.textColor = [UIColor blackColor];
        _titleLabel.font = [UIFont systemFontOfSize:11];
        _titleLabel.textAlignment = NSTextAlignmentCenter;
    }
    return _titleLabel;
}

4. Summary

iOS development – download CGFontRef using network special fonts

During the development, when the font needs to be downloaded and then displayed, this special font can only be normal after downloading. .

Learning records, keep improving every day.