在 iOS 中實作 Azure Blob Storage 上傳進度條

大家都知道在 App 上傳檔案時顯示進度條是讓使用者知道上傳進度是友善的設計,某些情況下當你開發 iOS App 需要上傳檔案到 Azure Blob Storage 卻可能不知道該怎麼下手,本篇文章能讓你和你的團隊看到一絲曙光。

不熟 Objective-C 和 Cocoa Framework 的你 (好啦就是我本人怎樣) 可能會嘗試著以物件導向的方式繼承 NSInputStream 以責任鍊的方式攔截讀取方法,取得目前位置和檔案長度,但多次嘗試後你會很挫折。想要以 Observer 或 NotificationCenter 的方式解決也無從下手。

試了好久我都差不多快要放棄了哈哈。

回到正題,原不具上傳進度資訊的原程式碼如下:

#import <AZSClient/AZSClient.h>

NSString *const AZURE_STORAGE_CONNECTION_STRING = @"DefaultEndpointsProtocol=https;AccountName=(your account name);AccountKey=(your account key);EndpointSuffix=core.windows.net";
NSString *const AZURE_STORAGE_CONTAINER = @"files";
NSString *const LOCAL_FILE_FILEPATH = @"/Users/dino/A.mp4";
NSString *const LOCAL_FILE_FILENAME = @"A.mp4";

NSError *error;
AZSCloudStorageAccount *account = [AZSCloudStorageAccount accountFromConnectionString:AZURE_STORAGE_CONNECTION_STRING error:&error];
AZSCloudBlobClient *client = [account getBlobClient];
AZSCloudBlobContainer *container = [client containerReferenceFromName:AZURE_STORAGE_CONTAINER];

[container createContainerIfNotExistsWithCompletionHandler:^(NSError *error, BOOL exists) {
    @autoreleasepool {
        AZSCloudBlockBlob *blob = [container blockBlobReferenceFromName:LOCAL_FILE_FILENAME];
        
        NSInputStream *stream = [NSInputStream inputStreamWithFileAtPath:LOCAL_FILE_FILEPATH];
        
        NSLog(@"upload started.");
        
        [blob uploadFromStream:stream completionHandler:^(NSError *error) {
             NSLog(@"upload done.");
             exit(0);
        }];
    }
}];

解法在原始碼中,讀完程式碼後我們發現 AZSOperationContext 類別提供我們解套的方式。AZSOperationContext 提供第三方介入程式庫,提供事件、預設屬性、日誌等讓開發人員得以自訂的機會。

我們可以運用 sendRequest 事件,透過 NSStreamFileCurrentOffsetKey 讀取的 NSInputStream 的指標位置,搭配已知的檔案長度,即可計算得出完成百分比。

#import <AZSClient/AZSClient.h>

NSString *const AZURE_STORAGE_CONNECTION_STRING = @"DefaultEndpointsProtocol=https;AccountName=(your account name);AccountKey=(your account key);EndpointSuffix=core.windows.net";
NSString *const AZURE_STORAGE_CONTAINER = @"files";
NSString *const LOCAL_FILE_FILEPATH = @"/Users/dino/A.mp4";
NSString *const LOCAL_FILE_FILENAME = @"A.mp4";

NSError *error;
AZSCloudStorageAccount *account = [AZSCloudStorageAccount accountFromConnectionString:AZURE_STORAGE_CONNECTION_STRING error:&error];
AZSCloudBlobClient *client = [account getBlobClient];
AZSCloudBlobContainer *container = [client containerReferenceFromName:AZURE_STORAGE_CONTAINER];

[container createContainerIfNotExistsWithCompletionHandler:^(NSError *error, BOOL exists) {
    @autoreleasepool {
        AZSCloudBlockBlob *blob = [container blockBlobReferenceFromName:LOCAL_FILE_FILENAME];
        
        NSUInteger size = [[[NSFileManager defaultManager] attributesOfItemAtPath:LOCAL_FILE_FILEPATH error:&error] fileSize];
        
        NSInputStream *stream = [NSInputStream inputStreamWithFileAtPath:LOCAL_FILE_FILEPATH];
        
        AZSOperationContext *operationContext = [[AZSOperationContext alloc] init];
        operationContext.sendingRequest = ^(NSMutableURLRequest *request, AZSOperationContext *context) {
            NSNumber *number = [stream propertyForKey:NSStreamFileCurrentOffsetKey];
            if (number != nil) {
                NSUInteger pos = [number longValue];
                float percentage = ((float)pos / size) * 100;
                NSLog(@"current %ld of %ld, %f%% completed", pos, size, percentage);
            }
        };
        
        NSLog(@"upload started.");
        
        [blob uploadFromStream:stream accessCondition:nil requestOptions:nil operationContext:operationContext completionHandler:^(NSError *error) {
             NSLog(@"upload done.");
             exit(0);
        }];
    }
}];