WKWebView & UIWebView 进度条动画

导语

 

本文目的是实现一个网络请求进度条的动画效果,主要结构分为以下三个部分

  • JAProgressWKWebView : 使用 WKWebView 的场景
  • JAProgressUIWebView : 使用 UIWebView 的场景
  • JAProgressView : 一般情况下使用 NSURLSession 的场景

环境

 

macOS Sierra 10.12.4
Xcode 8.3.2
iPhone 6S (10.1.1)
iPad Mini 2 (8.4)

WKWebView

 

A WKWebView object displays interactive web content, such as for an in-app browser. You can use the WKWebView class to embed web content in your app. To do so, create a WKWebView object, set it as the view, and send it a request to load web content.
Important
Starting in iOS 8.0 and OS X 10.10, use WKWebView to add web content to your app. Do not use UIWebView or WebView.

WKWebViewAppleiOS 8 时用于替代 UIWebView 的控件

原理

 

WKWebView 控件提供了如下的属性,用于描述当前页面的加载进度,采用 KVO 的方式,获得进度值并进行相应的操作。

 

1
2
3
4
5
6
7
8
9
10
11
 WKWebView.h

/*! @abstract An estimate of what fraction of the current navigation has been completed.
@discussion This value ranges from 0.0 to 1.0 based on the total number of
bytes expected to be received, including the main document and all of its
potential subresources. After a navigation completes, the value remains at 1.0
until a new navigation starts, at which point it is reset to 0.0.
@link WKWebView @/link is key-value observing (KVO) compliant for this
property.
*/
@property (nonatomic, readonly) double estimatedProgress;

实践

 

添加观察者

1
2
3
4
5
6
7
- (instancetype)initWithFrame:(CGRect)frame configuration:(WKWebViewConfiguration *)configuration {
if (self = [super initWithFrame:frame configuration:configuration]) {
[self setup];
[self addObserver:self forKeyPath:JAkEstimatedProgress options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}
return self;
}

刷新进度

1
2
3
4
5
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {

double newKey = [[change objectForKey:NSKeyValueChangeNewKey] doubleValue];
[self.progressBarlayer flush:newKey];
}

移除观察者

1
2
3
4
5
6
7
- (void)dealloc {
@try {
[self removeObserver:self forKeyPath:JAkEstimatedProgress];
} @catch (NSException *exception) {
// NSLog(@"%@",exception);
}
}

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#import "JAWKViewController.h"
#import "JAProgressView.h"

@interface JAWKViewController () <WKNavigationDelegate>

@property (nonatomic,strong) JAProgressWKWebView *webView;

@end

@implementation JAWKViewController

- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor colorWithWhite:0.92 alpha:1.0];
self.automaticallyAdjustsScrollViewInsets = false;
WKWebViewConfiguration *theConfiguration = [[WKWebViewConfiguration alloc] init];
JAProgressWKWebView *webView = [[JAProgressWKWebView alloc] initWithFrame:CGRectMake(0, 64, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height - 64) configuration:theConfiguration];
webView.backgroundColor = [UIColor groupTableViewBackgroundColor];
webView.opaque = false;
webView.navigationDelegate = self;
[self.view addSubview:_webView = webView];
// [self.navigationController.navigationBar addSubview:webView.progressView];
}

- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
NSURL *nsurl=[NSURL URLWithString:self.urlString];
NSURLRequest *nsrequest=[NSURLRequest requestWithURL:nsurl];
[_webView loadRequest:nsrequest];
}

- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
NSLog(@"加载完成");
}

- (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error {
NSLog(@"加载失败");
[_webView finish];
}

- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
}

@end

UIWebView

 

You can use the UIWebView class to embed web content in your app. To do so, create a UIWebView object, attach it to a window, and send it a request to load web content. You can also use this class to move back and forward in the history of webpages, and you can even set some web content properties programmatically.
Note
In apps that run in iOS 8 and later, use the WKWebView class instead of using UIWebView. Additionally, consider setting the WKPreferences property javaScriptEnabled to NO if you render files that are not supposed to run JavaScript.

原理

 

先叉开一下话题,看看造好的轮子是如何实现检测网络进度的。下面以 AFNetworking 为例进行说明

AFN 中有一个 UIWebView+AFNetworking.h 文件,是为 UIWebView 添加的分类,里面有

1
2
3
4
- (void)loadRequest:(NSURLRequest *)request
progress:(NSProgress * _Nullable __autoreleasing * _Nullable)progress
success:(nullable NSString * (^)(NSHTTPURLResponse *response, NSString *HTML))success
failure:(nullable void (^)(NSError *error))failure;

最终调用的是调用了下面的方法

1
2
3
4
5
6
- (void)loadRequest:(NSURLRequest *)request
MIMEType:(nullable NSString *)MIMEType
textEncodingName:(nullable NSString *)textEncodingName
progress:(NSProgress * _Nullable __autoreleasing * _Nullable)progress
success:(nullable NSData * (^)(NSHTTPURLResponse *response, NSData *data))success
failure:(nullable void (^)(NSError *error))failure;

关注 progress 参数,要求传递的是一个 progress 对象的地址

1
2
3
4
5
6
7
NSURLSessionDataTask *dataTask;
...
self.af_URLSessionTask = dataTask;
if (progress != nil) {
*progress = [self.sessionManager downloadProgressForTask:dataTask];
}
[self.af_URLSessionTask resume];

顾名思义 progress 会接收 downloadProgressForTask: 方法的返回值,即本次请求的进度

进到 downloadProgressForTask: 方法中

1
2
3
- (NSProgress *)downloadProgressForTask:(NSURLSessionTask *)task {
return [[self delegateForTask:task] downloadProgress];
}

查看 downloadProgress 属性在哪个类中

1
2
3
@interface AFURLSessionManagerTaskDelegate : NSObject <NSURLSessionTaskDelegate, NSURLSessionDataDelegate, NSURLSessionDownloadDelegate>
...
@property (nonatomic, strong) NSProgress *downloadProgress;

在文件中以 downloadProgress 进行搜索会发现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#pragma mark - NSProgress Tracking

- (void)setupProgressForTask:(NSURLSessionTask *)task {
__weak __typeof__(task) weakTask = task;
...
[task addObserver:self
forKeyPath:NSStringFromSelector(@selector(countOfBytesReceived))
options:NSKeyValueObservingOptionNew
context:NULL];

[task addObserver:self
forKeyPath:NSStringFromSelector(@selector(countOfBytesExpectedToReceive))
options:NSKeyValueObservingOptionNew
context:NULL];

...

[self.downloadProgress addObserver:self
forKeyPath:NSStringFromSelector(@selector(fractionCompleted))
options:NSKeyValueObservingOptionNew
context:NULL];
...

找到响应的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
if ([object isKindOfClass:[NSURLSessionTask class]] || [object isKindOfClass:[NSURLSessionDownloadTask class]]) {
if ([keyPath isEqualToString:NSStringFromSelector(@selector(countOfBytesReceived))]) {
self.downloadProgress.completedUnitCount = [change[NSKeyValueChangeNewKey] longLongValue];
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(countOfBytesExpectedToReceive))]) {
self.downloadProgress.totalUnitCount = [change[NSKeyValueChangeNewKey] longLongValue];
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(countOfBytesSent))]) {
self.uploadProgress.completedUnitCount = [change[NSKeyValueChangeNewKey] longLongValue];
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(countOfBytesExpectedToSend))]) {
self.uploadProgress.totalUnitCount = [change[NSKeyValueChangeNewKey] longLongValue];
}
}
...

进度的计算是根据 NSURLSession 的两个只读的属性 countOfBytesReceived & countOfBytesExpectedToReceive ,前者是已接收到的数据长度,后者是期望接收到的总长度,来自于 Http 头中的 Content-Length 字段,通过接收这两个值的变化对应去修改 NSProgress 中的 completedUnitCount & totalUnitCount 这两个值

1
2
3
4
5
6
/* The size of the job whose progress is being reported, and how much of it has been completed so far, respectively. For an NSProgress with a kind of NSProgressKindFile, the unit of these properties is bytes while the NSProgressFileTotalCountKey and NSProgressFileCompletedCountKey keys in the userInfo dictionary are used for the overall count of files. For any other kind of NSProgress, the unit of measurement you use does not matter as long as you are consistent. The values may be reported to the user in the localizedDescription and localizedAdditionalDescription.

If the receiver NSProgress object is a "leaf progress" (no children), then the fractionCompleted is generally completedUnitCount / totalUnitCount. If the receiver NSProgress has children, the fractionCompleted will reflect progress made in child objects in addition to its own completedUnitCount. As children finish, the completedUnitCount of the parent will be updated.
*/
@property int64_t totalUnitCount;
@property int64_t completedUnitCount;

fractionCompleted 属性是根据 completedUnitCount / totalUnitCount 得到的。

因此很自然的一种思路是去监听传递的 progress 参数的 fractionCompleted

再此之前先设断点证明下确实走了 setupProgressForTask:

同时因为 AFURLSessionManagerTaskDelegate 并没有暴露给外部,不去修改框架比较好 KVO 是一对多的,也算比较合适。

可添加类似如下的代码对 progress 参数的 fractionCompleted 属性进行观察

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
NSURL *nsurl=[NSURL URLWithString:self.urlString];
NSURLRequest *nsrequest=[NSURLRequest requestWithURL:nsurl];
NSProgress *progress = [[NSProgress alloc] init];
[_webView loadRequest:nsrequest progress:&progress success:^NSString * _Nonnull(NSHTTPURLResponse * _Nonnull response, NSString * _Nonnull HTML) {
return @"";
} failure:^(NSError * _Nonnull error) {

}];

[progress addObserver:_webView forKeyPath:@"fractionCompleted" options:NSKeyValueObservingOptionNew context:NULL];
}

#pragma mark - UIWebViewDelegate
- (void)webViewDidFinishLoad:(UIWebView *)webView {
NSLog(@"加载完成");
}

但是会发现 _webView 对象中的方法

1
2
3
4
5
6
7
8
9
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
dispatch_async(dispatch_get_main_queue(), ^{
double newKey = [[change objectForKey:NSKeyValueChangeNewKey] doubleValue];
[self.progressBarlayer flush:newKey];
if (1 - newKey < 0.01) {
[self finish];
}
});
}

并没有被执行,跟踪 AFNetowrking

progressfractionCompleted 一直都为 0 因为 countOfBytesExpectedToReceive 的总长度是 -1 (0xffffffffffffffff) 这一点可以在 issues - Progress and KVO never called 得到佐证,主要是因为请求头的 Content-Length 字段未设置。

访问 https://www.baidu.com

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
0x0000000000000b6d

2925

(lldb) po dataTask.response
<NSHTTPURLResponse: 0x174030160> { URL: https://www.baidu.com/ } { status code: 200, headers {
Connection = "keep-alive";
"Content-Encoding" = gzip;
"Content-Type" = "text/html";
Date = "Sat, 13 May 2017 09:14:00 GMT";
P3p = "CP=\" OTI DSP COR IVA OUR IND COM \"";
Server = "bfe/1.0.8.18";
"Set-Cookie" = "BAIDUID=623A610A25264EC09CC77FECD44A7CE4:FG=1; max-age=31536000; expires=Sun, 13-May-18 09:14:00 GMT; domain=.baidu.com; path=/; version=1, H_WISE_SIDS=102431; path=/; domain=.baidu.com, BDSVRTM=9; path=/, __bsi=13135479753770821857_00_33_N_N_11_0303_C02F_N_N_Y_0; expires=Sat, 13-May-17 09:14:05 GMT; domain=www.baidu.com; path=/";
Traceid = 149466684009534243944441198276626132688;
"Transfer-Encoding" = Identity;
Vary = "Accept-Encoding";
} }

(lldb) po dataTask.countOfBytesExpectedToReceive
2925

(lldb)

访问 https://www.bing.com

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
0xffffffffffffffff

-1

(lldb) po dataTask.response
<NSHTTPURLResponse: 0x17022c6c0> { URL: https://www.bing.com/ } { status code: 200, headers {
"Cache-Control" = "private, max-age=0";
"Content-Encoding" = gzip;
"Content-Length" = 32317;
"Content-Type" = "text/html; charset=utf-8";
Date = "Sat, 13 May 2017 09:16:11 GMT";
P3P = "CP=\"NON UNI COM NAV STA LOC CURa DEVa PSAa PSDa OUR IND\"";
Server = "Microsoft-IIS/10.0";
"Set-Cookie" = "_SS=SID=3C3C051FE4516B5A18710F9EE5F06A6F; domain=.bing.com; path=/, _EDGE_S=SID=3C3C051FE4516B5A18710F9EE5F06A6F; path=/; httponly; domain=bing.com";
Vary = "Accept-Encoding";
"X-MSEdge-Ref" = "Ref A: 0505C24953B941CA96CF74FAE726B298 Ref B: BJ1EDGE0322 Ref C: Sat May 13 02:16:11 2017 PST";
} }

(lldb) po dataTask.countOfBytesExpectedToReceive
-1

(lldb)

访问 https://www.google.com

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
0xffffffffffffffff

-1

(lldb) po dataTask.response
<NSHTTPURLResponse: 0x170033180> { URL: https://www.google.com/ } { status code: 200, headers {
"Alt-Svc" = "quic=\":443\"; ma=2592000; v=\"37,36,35\"";
"Cache-Control" = private;
"Content-Encoding" = gzip;
"Content-Type" = "text/html; charset=Big5";
Date = "Sat, 13 May 2017 09:24:46 GMT";
Expires = "Sat, 13 May 2017 09:24:46 GMT";
P3P = "CP=\"This is not a P3P policy! See https://www.google.com/support/accounts/answer/151657?hl=en for more info.\"";
Server = gws;
"Set-Cookie" = "NID=103=adCbLI6_cKax8zD5WTTC6Xq-Vviron4fGJDZeVf1OEiQhvSD9L3Q53n8HrmZS8--BUJIIAbR_cb5UwweP0nvyOKD0J4tbOLf6tr-p_Vva5mnAiYyWKKzsOhqtc9SjBhW; expires=Sun, 12-Nov-2017 09:24:46 GMT; path=/; domain=.google.com; HttpOnly";
"Transfer-Encoding" = Identity;
"X-Frame-Options" = SAMEORIGIN;
"X-XSS-Protection" = "1; mode=block";
} }

(lldb) po dataTask.countOfBytesExpectedToReceive
-1

第二次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
0x0000000000002ae7


- Hook 1 (expr -- @import UIKit)

- Hook 2 ( target stop-hook disable)
10983

(lldb) po dataTask.response
<NSHTTPURLResponse: 0x17003cd60> { URL: https://www.google.com/ } { status code: 200, headers {
"Alt-Svc" = "quic=\":443\"; ma=2592000; v=\"37,36,35\"";
"Cache-Control" = private;
"Content-Encoding" = gzip;
"Content-Type" = "text/html; charset=Big5";
Date = "Sat, 13 May 2017 10:09:01 GMT";
Expires = "Sat, 13 May 2017 10:09:01 GMT";
P3P = "CP=\"This is not a P3P policy! See https://www.google.com/support/accounts/answer/151657?hl=en for more info.\"";
Server = gws;
"Set-Cookie" = "NID=103=f7RScaRo91vaPuGoStsjYJrDLUSANq6fwzBypLCvMeUasdsx443kwCfnzioj167ke0Buh6c6qVsA_xiT8olCjCvOtOd4P9drblpB5PkDMGiWg0J6qJnBuhoeFQIREDVT; expires=Sun, 12-Nov-2017 10:09:01 GMT; path=/; domain=.google.com; HttpOnly";
"Transfer-Encoding" = Identity;
"X-Frame-Options" = SAMEORIGIN;
"X-XSS-Protection" = "1; mode=block";
} }

(lldb) po dataTask.countOfBytesExpectedToReceive
10983
名称 Content-Length countOfBytesExpectedToReceive
https://www.baidu.com ✔️
https://www.bing.com ✔️
https://www.google.com ❎ / ✔️

搜索 Content-Length 字段的相关内容

rfc2616.html#header.content-length

rfc2616.html#message.length

HTTP 1.1 中应用可以通过 Content-Lenght 字段确定消息的长度,

Content-Length 失效的几种情况

  • 状态码为 1xx,204304
  • 设置了 Transfer-Encoding 且该值不等于 identity
  • 同时存在 Transfer-EncodingContent-Length
  • 响应的数据类型为 multipart/byteranges 且没有指定 transfer-length
  • 服务器关闭连接

这里

1、在Http 1.0及之前版本中,content-length字段可有可无。
2、在http1.1及之后版本。如果是keep alive,则content-length和chunk必然是二选一。若是非keep alive,则和http1.0一样。content-length可有可无。

Transfer-Encoding

 

用来指定数据传输的格式,有如下的格式

Transfer-Encoding: chunked
Transfer-Encoding: compress
Transfer-Encoding: deflate
Transfer-Encoding: gzip
Transfer-Encoding: identity

chunked 的格式详情可以参考 Wiki-分块传输编码

如果一个HTTP消息(请求消息或应答消息)的Transfer-Encoding消息头的值为chunked,那么,消息体由数量未定的块组成,并以最后一个大小为0的块为结束。
每一个非空的块都以该块包含数据的字节数(字节数以十六进制表示)开始,跟随一个CRLF (回车及换行),然后是数据本身,最后块CRLF结束。在一些实现中,块大小和CRLF之间填充有白空格(0x20)。
最后一块是单行,由块大小(0),一些可选的填充白空格,以及CRLF。最后一块不再包含任何数据,但是可以发送可选的尾部,包括消息头字段。
消息最后以CRLF结尾。

但是到这里依然没完全解决我的困惑,在 iOS 中如何比较获得网络请求的总长度?

寻找头文件后发现,在 NSURLResponse 对象中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*! 
@method expectedContentLength
@abstract Returns the expected content length of the receiver.
@discussion Some protocol implementations report a content length
as part of delivering load metadata, but not all protocols
guarantee the amount of data that will be delivered in actuality.
Hence, this method returns an expected amount. Clients should use
this value as an advisory, and should be prepared to deal with
either more or less data.
@result The expected content length of the receiver, or -1 if
there is no expectation that can be arrived at regarding expected
content length.
*/
@property (readonly) long long expectedContentLength;

我的方案

 

经过测试,在 NSURLSessionDataDelegate 的代理方法URLSession:dataTask:didReceiveData: , data 即为每次获取的数据,而 dataTask 中的 response 属性的 expectedContentLength 有时会在数据并没有完全接收到时就可以获得长度,因此对 UIWebView 用这个值来获取网络请求进度,采用的主要思路是

  • 在未获得 expectedContentLength 长度时,用数组保存每次 data 的值
  • 获取到 expectedContentLength , 根据数组中的值算出进度,在本地实现动画。

网络请求暂时决定还是依赖 AFNetworking , 添加了一个分类,在运行时替换 URLSession:dataTask:didReceiveData: 方法来实现

实践

 

分类 AFHTTPSessionManager+JACoder.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#import "AFHTTPSessionManager+JACoder.h"
#import <objc/message.h>

NSString *kAFJAReceiveDataNotification = @"kAFNReceiveDataNotification";
NSString *kAFJAReceiveResponseNotification = @"kAFJAReceiveResponseNotification";

@implementation AFHTTPSessionManager (JACoder)

+ (void)load {
Method originalMethod = class_getInstanceMethod(self, @selector(URLSession:dataTask:didReceiveData:));
Method swizzledMethod = class_getInstanceMethod(self, @selector(ja_URLSession:dataTask:didReceiveData:));
BOOL didAddMethod =
class_addMethod(self,
@selector(URLSession:dataTask:didReceiveData:),
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));

if (didAddMethod) {
class_replaceMethod(self,
@selector(ja_URLSession:dataTask:didReceiveData:),
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}

- (void)ja_URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveData:(NSData *)data {

[self ja_URLSession:session dataTask:dataTask didReceiveData:data];

// ...
if (dataTask.response.expectedContentLength != -1 && self.isLock == false) {
// NSLog(@"data = %ld",data.length);
// NSLog(@"dataTask = %lld",dataTask.response.expectedContentLength);
[[NSNotificationCenter defaultCenter] postNotificationName:kAFJAReceiveDataNotification object:data];
[[NSNotificationCenter defaultCenter] postNotificationName:kAFJAReceiveResponseNotification object:dataTask];

self.lock = true;

}else {

// NSLog(@"data = %ld",data.length);
[[NSNotificationCenter defaultCenter] postNotificationName:kAFJAReceiveDataNotification object:data];
}
}

- (BOOL)isLock {
return (BOOL)[objc_getAssociatedObject(self, @selector(isLock)) doubleValue];
}

- (void)setLock:(BOOL)lock {
objc_setAssociatedObject(self, @selector(setLock:), @(lock), OBJC_ASSOCIATION_ASSIGN);
}

@end

接收到已获取总长度的通知后的相应处理,为了和 WKWebView 统一,在继承 UIWebView 的子类中添加了 estimatedProgress 属性,同样用 KVO 去处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
- (void)receiveWithNotification:(NSNotification *)noti {
if ([noti.object isKindOfClass:[NSURLSessionDataTask class]]) {
NSURLSessionDataTask *dataTask = (NSURLSessionDataTask *)noti.object;
self.expectedContentLength = dataTask.response.expectedContentLength;

if (self.estimatedProgress == 0 ) {
if (self.records.count == 0) {
self.estimatedProgress = 1.0;
}else {
for (NSData *record in self.records) {
self.estimatedProgress += (CGFloat)record.length / self.expectedContentLength;
}
self.records = [NSArray array];
}
}else {
for (NSData *record in self.records) {
self.estimatedProgress += (CGFloat)record.length / self.expectedContentLength;
}
}
// NSLog(@"noti dataTask:%f",self.estimatedProgress);
}

if ([noti.object isKindOfClass:[NSData class]]) {
NSData *data = (NSData *)noti.object;
if (self.expectedContentLength != -1 && self.expectedContentLength != 0) {
for (NSData *record in self.records) {
self.estimatedProgress += (CGFloat)record.length / self.expectedContentLength;
}
self.estimatedProgress += (CGFloat)data.length / self.expectedContentLength;
// NSLog(@"noti data:%f",self.estimatedProgress);
self.records = [NSArray array];
}else {
NSMutableArray *recordsM = [NSMutableArray arrayWithArray:self.records];
[recordsM addObject:data];
self.records = [recordsM copy];
}
}
}

estimatedProgress 属性变化的处理

1
2
3
4
5
6
7
8
9
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
dispatch_async(dispatch_get_main_queue(), ^{
double newKey = [[change objectForKey:NSKeyValueChangeNewKey] doubleValue];
[self.progressBarlayer flush:newKey];
if (1 - newKey < 0.01) {
[self finish];
}
});
}

UIView

 

对于一般的 App 请求,大多都是返回 JSON 数据的接口,也可以做一个进度条的效果。

原理

 

一事不烦二主,也用 AFNetworking 做请求。上面已经分析了 AFNetworking 如何检测请求进度的, 大多数情况下的请求进度肯定是没问题的。

实践

 

AFNetworking 发起请求,根据回调返回的 NSProgress 值实现刷新操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#import "JAUIViewController.h"
#import <AFNetworking.h>
#import "JAProgressView.h"

@interface JAUIViewController ()

@property (nonatomic,strong) JAProgressView *progressView;

@end

@implementation JAUIViewController

- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor colorWithWhite:0.92 alpha:1.0];
_progressView = [[JAProgressView alloc] init];
[self.navigationController.navigationBar addSubview:_progressView];
}

- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[[AFHTTPSessionManager manager] GET:@"http://api.map.baidu.com/telematics/v3/weather?location=%E5%98%89%E5%85%B4&output=json&ak=5slgyqGDENN7Sy7pw29IUvrZ" parameters:nil progress:^(NSProgress * _Nonnull downloadProgress) {

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[_progressView flush:downloadProgress.fractionCompleted];
});

// or
// [[NSNotificationCenter defaultCenter] postNotificationName:JAEstimatedProgressNotification object:downloadProgress];

} success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {

} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {

}];
}

- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
}

相应的处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 通知
- (void)receiveWithNotification:(NSNotification *)notifcation {
dispatch_async(dispatch_get_main_queue(), ^{
if ([notifcation.object isKindOfClass:[NSProgress class]]) {
NSProgress *progress = (NSProgress *)notifcation.object;
[self.progressBarlayer flush:progress.fractionCompleted];
}
});
}

// 传值
- (void)flush:(CGFloat)progress {
[self.progressBarlayer flush:progress];
}

效果图

 

DEMO

参考

 

  1. Progress and KVO never called
  2. Wiki-分块传输编码
  3. MDN - Transfer-Encoding
  4. http协议中content-length 以及chunked编码分析