Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
long-awaited arrival of a loading progress bar
- Loading branch information
Showing
10 changed files
with
436 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
The MIT License (MIT) | ||
|
||
Copyright (c) 2013 Satoshi Asano | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in | ||
all copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||
THE SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
// | ||
// NJKWebViewProgress.h | ||
// | ||
// Created by Satoshi Aasano on 4/20/13. | ||
// Copyright (c) 2013 Satoshi Asano. All rights reserved. | ||
// | ||
|
||
#import <Foundation/Foundation.h> | ||
|
||
#undef njk_weak | ||
#if __has_feature(objc_arc_weak) | ||
#define njk_weak weak | ||
#else | ||
#define njk_weak unsafe_unretained | ||
#endif | ||
|
||
typedef void (^NJKWebViewProgressBlock)(float progress); | ||
@protocol NJKWebViewProgressDelegate; | ||
@interface NJKWebViewProgress : NSObject<UIWebViewDelegate> | ||
@property (nonatomic, njk_weak) id<NJKWebViewProgressDelegate>progressDelegate; | ||
@property (nonatomic, njk_weak) id<UIWebViewDelegate>webViewProxyDelegate; | ||
@property (nonatomic, copy) NJKWebViewProgressBlock progressBlock; | ||
@property (nonatomic, readonly) float progress; // 0.0..1.0 | ||
|
||
- (void)reset; | ||
@end | ||
|
||
@protocol NJKWebViewProgressDelegate <NSObject> | ||
- (void)webViewProgress:(NJKWebViewProgress *)webViewProgress updateProgress:(float)progress; | ||
@end | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,193 @@ | ||
// | ||
// NJKWebViewProgress.m | ||
// | ||
// Created by Satoshi Aasano on 4/20/13. | ||
// Copyright (c) 2013 Satoshi Asano. All rights reserved. | ||
// | ||
|
||
#import "NJKWebViewProgress.h" | ||
|
||
NSString *completeRPCURL = @"webviewprogressproxy:///complete"; | ||
|
||
static const float initialProgressValue = 0.1; | ||
static const float beforeInteractiveMaxProgressValue = 0.5; | ||
static const float afterInteractiveMaxProgressValue = 0.9; | ||
|
||
@implementation NJKWebViewProgress | ||
{ | ||
NSUInteger _loadingCount; | ||
NSUInteger _maxLoadCount; | ||
NSURL *_currentURL; | ||
BOOL _interactive; | ||
} | ||
|
||
- (id)init | ||
{ | ||
self = [super init]; | ||
if (self) { | ||
_maxLoadCount = _loadingCount = 0; | ||
_interactive = NO; | ||
} | ||
return self; | ||
} | ||
|
||
- (void)startProgress | ||
{ | ||
if (_progress < initialProgressValue) { | ||
[self setProgress:initialProgressValue]; | ||
} | ||
} | ||
|
||
- (void)incrementProgress | ||
{ | ||
float progress = self.progress; | ||
float maxProgress = _interactive ? afterInteractiveMaxProgressValue : beforeInteractiveMaxProgressValue; | ||
float remainPercent = (float)_loadingCount / (float)_maxLoadCount; | ||
float increment = (maxProgress - progress) * remainPercent; | ||
progress += increment; | ||
progress = fmin(progress, maxProgress); | ||
[self setProgress:progress]; | ||
} | ||
|
||
- (void)completeProgress | ||
{ | ||
[self setProgress:1.0]; | ||
} | ||
|
||
- (void)setProgress:(float)progress | ||
{ | ||
// progress should be incremental only | ||
if (progress > _progress || progress == 0) { | ||
_progress = progress; | ||
if ([_progressDelegate respondsToSelector:@selector(webViewProgress:updateProgress:)]) { | ||
[_progressDelegate webViewProgress:self updateProgress:progress]; | ||
} | ||
if (_progressBlock) { | ||
_progressBlock(progress); | ||
} | ||
} | ||
} | ||
|
||
- (void)reset | ||
{ | ||
_maxLoadCount = _loadingCount = 0; | ||
_interactive = NO; | ||
[self setProgress:0.0]; | ||
} | ||
|
||
#pragma mark - | ||
#pragma mark UIWebViewDelegate | ||
|
||
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType | ||
{ | ||
if ([request.URL.absoluteString isEqualToString:completeRPCURL]) { | ||
[self completeProgress]; | ||
return NO; | ||
} | ||
|
||
BOOL ret = YES; | ||
if ([_webViewProxyDelegate respondsToSelector:@selector(webView:shouldStartLoadWithRequest:navigationType:)]) { | ||
ret = [_webViewProxyDelegate webView:webView shouldStartLoadWithRequest:request navigationType:navigationType]; | ||
} | ||
|
||
BOOL isFragmentJump = NO; | ||
if (request.URL.fragment) { | ||
NSString *nonFragmentURL = [request.URL.absoluteString stringByReplacingOccurrencesOfString:[@"#" stringByAppendingString:request.URL.fragment] withString:@""]; | ||
isFragmentJump = [nonFragmentURL isEqualToString:webView.request.URL.absoluteString]; | ||
} | ||
|
||
BOOL isTopLevelNavigation = [request.mainDocumentURL isEqual:request.URL]; | ||
|
||
BOOL isHTTP = [request.URL.scheme isEqualToString:@"http"] || [request.URL.scheme isEqualToString:@"https"]; | ||
if (ret && !isFragmentJump && isHTTP && isTopLevelNavigation) { | ||
_currentURL = request.URL; | ||
[self reset]; | ||
} | ||
return ret; | ||
} | ||
|
||
- (void)webViewDidStartLoad:(UIWebView *)webView | ||
{ | ||
if ([_webViewProxyDelegate respondsToSelector:@selector(webViewDidStartLoad:)]) { | ||
[_webViewProxyDelegate webViewDidStartLoad:webView]; | ||
} | ||
|
||
_loadingCount++; | ||
_maxLoadCount = fmax(_maxLoadCount, _loadingCount); | ||
|
||
[self startProgress]; | ||
} | ||
|
||
- (void)webViewDidFinishLoad:(UIWebView *)webView | ||
{ | ||
if ([_webViewProxyDelegate respondsToSelector:@selector(webViewDidFinishLoad:)]) { | ||
[_webViewProxyDelegate webViewDidFinishLoad:webView]; | ||
} | ||
|
||
_loadingCount--; | ||
[self incrementProgress]; | ||
|
||
NSString *readyState = [webView stringByEvaluatingJavaScriptFromString:@"document.readyState"]; | ||
|
||
BOOL interactive = [readyState isEqualToString:@"interactive"]; | ||
if (interactive) { | ||
_interactive = YES; | ||
NSString *waitForCompleteJS = [NSString stringWithFormat:@"window.addEventListener('load',function() { var iframe = document.createElement('iframe'); iframe.style.display = 'none'; iframe.src = '%@'; document.body.appendChild(iframe); }, false);", completeRPCURL]; | ||
[webView stringByEvaluatingJavaScriptFromString:waitForCompleteJS]; | ||
} | ||
|
||
BOOL isNotRedirect = _currentURL && [_currentURL isEqual:webView.request.mainDocumentURL]; | ||
BOOL complete = [readyState isEqualToString:@"complete"]; | ||
if (complete && isNotRedirect) { | ||
[self completeProgress]; | ||
} | ||
} | ||
|
||
- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error | ||
{ | ||
if ([_webViewProxyDelegate respondsToSelector:@selector(webView:didFailLoadWithError:)]) { | ||
[_webViewProxyDelegate webView:webView didFailLoadWithError:error]; | ||
} | ||
|
||
_loadingCount--; | ||
[self incrementProgress]; | ||
|
||
NSString *readyState = [webView stringByEvaluatingJavaScriptFromString:@"document.readyState"]; | ||
|
||
BOOL interactive = [readyState isEqualToString:@"interactive"]; | ||
if (interactive) { | ||
_interactive = YES; | ||
NSString *waitForCompleteJS = [NSString stringWithFormat:@"window.addEventListener('load',function() { var iframe = document.createElement('iframe'); iframe.style.display = 'none'; iframe.src = '%@'; document.body.appendChild(iframe); }, false);", completeRPCURL]; | ||
[webView stringByEvaluatingJavaScriptFromString:waitForCompleteJS]; | ||
} | ||
|
||
BOOL isNotRedirect = _currentURL && [_currentURL isEqual:webView.request.mainDocumentURL]; | ||
BOOL complete = [readyState isEqualToString:@"complete"]; | ||
if (complete && isNotRedirect) { | ||
[self completeProgress]; | ||
} | ||
} | ||
|
||
#pragma mark - | ||
#pragma mark Method Forwarding | ||
// for future UIWebViewDelegate impl | ||
|
||
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector | ||
{ | ||
NSMethodSignature *signature = [super methodSignatureForSelector:selector]; | ||
if(!signature) { | ||
if([_webViewProxyDelegate respondsToSelector:selector]) { | ||
return [(NSObject *)_webViewProxyDelegate methodSignatureForSelector:selector]; | ||
} | ||
} | ||
return signature; | ||
} | ||
|
||
- (void)forwardInvocation:(NSInvocation*)invocation | ||
{ | ||
if ([_webViewProxyDelegate respondsToSelector:[invocation selector]]) { | ||
[invocation invokeWithTarget:_webViewProxyDelegate]; | ||
} | ||
} | ||
|
||
@end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
// | ||
// NJKWebViewProgressView.h | ||
// iOS 7 Style WebView Progress Bar | ||
// | ||
// Created by Satoshi Aasano on 11/16/13. | ||
// Copyright (c) 2013 Satoshi Asano. All rights reserved. | ||
// | ||
|
||
#import <UIKit/UIKit.h> | ||
|
||
@interface NJKWebViewProgressView : UIView | ||
@property (nonatomic) float progress; | ||
|
||
@property (nonatomic) UIView *progressBarView; | ||
@property (nonatomic) NSTimeInterval barAnimationDuration; // default 0.1 | ||
@property (nonatomic) NSTimeInterval fadeAnimationDuration; // default 0.27 | ||
@property (nonatomic) NSTimeInterval fadeOutDelay; // default 0.1 | ||
|
||
- (void)setProgress:(float)progress animated:(BOOL)animated; | ||
|
||
@end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
// | ||
// NJKWebViewProgressView.m | ||
// | ||
// Created by Satoshi Aasanoon 11/16/13. | ||
// Copyright (c) 2013 Satoshi Asano. All rights reserved. | ||
// | ||
|
||
#import "NJKWebViewProgressView.h" | ||
|
||
@implementation NJKWebViewProgressView | ||
|
||
- (id)initWithFrame:(CGRect)frame | ||
{ | ||
self = [super initWithFrame:frame]; | ||
if (self) { | ||
self.userInteractionEnabled = NO; | ||
self.autoresizingMask = UIViewAutoresizingFlexibleWidth; | ||
_progressBarView = [[UIView alloc] initWithFrame:self.bounds]; | ||
_progressBarView.autoresizingMask = UIViewAutoresizingFlexibleWidth; | ||
UIColor *tintColor = [UIColor colorWithRed:22.f / 255.f green:126.f / 255.f blue:251.f / 255.f alpha:1.0]; // iOS7 Safari bar color | ||
if ([UIApplication.sharedApplication.delegate.window respondsToSelector:@selector(setTintColor:)]) { | ||
tintColor = UIApplication.sharedApplication.delegate.window.tintColor; | ||
} | ||
_progressBarView.backgroundColor = tintColor; | ||
[self addSubview:_progressBarView]; | ||
|
||
_barAnimationDuration = 0.27f; | ||
_fadeAnimationDuration = 0.27f; | ||
_fadeOutDelay = 0.1f; | ||
} | ||
return self; | ||
} | ||
|
||
-(void)setProgress:(float)progress | ||
{ | ||
[self setProgress:progress animated:NO]; | ||
} | ||
|
||
- (void)setProgress:(float)progress animated:(BOOL)animated | ||
{ | ||
BOOL isGrowing = progress > 0.0; | ||
[UIView animateWithDuration:(isGrowing && animated) ? _barAnimationDuration : 0.0 delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{ | ||
CGRect frame = _progressBarView.frame; | ||
frame.size.width = progress * self.bounds.size.width; | ||
_progressBarView.frame = frame; | ||
} completion:nil]; | ||
|
||
if (progress >= 1.0) { | ||
[UIView animateWithDuration:animated ? _fadeAnimationDuration : 0.0 delay:_fadeOutDelay options:UIViewAnimationOptionCurveEaseInOut animations:^{ | ||
_progressBarView.alpha = 0.0; | ||
} completion:^(BOOL completed){ | ||
CGRect frame = _progressBarView.frame; | ||
frame.size.width = 0; | ||
_progressBarView.frame = frame; | ||
}]; | ||
} | ||
else { | ||
[UIView animateWithDuration:animated ? _fadeAnimationDuration : 0.0 delay:0.0 options:UIViewAnimationOptionCurveEaseInOut animations:^{ | ||
_progressBarView.alpha = 1.0; | ||
} completion:nil]; | ||
} | ||
} | ||
|
||
@end |
Oops, something went wrong.