Pitometsu/CruiserWebViewController

View on GitHub
Source/Classes/CruiserWebViewController.m

Summary

Maintainability
Test Coverage
//
//  CruiserWebViewController.m
//  CruiserWebViewController
//  https://github.com/dzenbot/CruiserWebViewController
//
//  Created by Ignacio Romero Zurbuchen on 10/25/13.
//  Improved by Yuriy Pitomets on 23/01/2015
//  Copyright (c) 2014 DZN Labs. All rights reserved.
//  Copyright (c) 2015 Yuriy Pitomets. No rights reserved.
//  Licence: MIT-Licence
//

#import "CruiserPolyActivity.h"
#import "CruiserWebViewController.h"


#define CRUISER_IS_IPAD [[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad
#define CRUISER_IS_LANDSCAPE \
([UIApplication sharedApplication].statusBarOrientation == UIInterfaceOrientationLandscapeLeft ||\
 [UIApplication sharedApplication].statusBarOrientation == UIInterfaceOrientationLandscapeRight)


static char CruiserWebViewControllerKVOContext       = 0;
static const NSTimeInterval kAnimationDuration       = 0.25;
static NSString *const kPinsDictionaryKey            = @"cruiser_web_view_controller.pins_dictionary";
static NSString *const kGoogleServiceRequestPath     = @"https://www.google.com/search?q=";
static NSString *const kDuckDuckGoServiceRequestPath = @"https://duckduckgo.com/?q=";
static NSString *const kHostnameRegex                = @"((\\w)*|([0-9]*)|([-|_])*)+"
                                                        "([\\.|/]((\\w)*|([0-9]*)|([-|_])*))+";


@interface CruiserWebViewController () <UIScrollViewDelegate>

@property (nonatomic, strong) UIBarButtonItem *backwardBarItem;
@property (nonatomic, strong) UIBarButtonItem *forwardBarItem;
@property (nonatomic, strong) UIBarButtonItem *pinBarItem;
@property (nonatomic, strong) UIBarButtonItem *downBarItem;
@property (nonatomic, strong) UIBarButtonItem *upBarItem;
@property (nonatomic, strong) UIBarButtonItem *actionBarItem;

@property (nonatomic, strong) UIButton *stateButton;
@property (nonatomic, strong) UIButton *webButton;

@property (nonatomic, assign) CruiserSearchService currentSearchService;

@property (nonatomic, strong) UIProgressView *progressView;

@property (nonatomic, strong) UILongPressGestureRecognizer *backwardLongPress;
@property (nonatomic, strong) UILongPressGestureRecognizer *forwardLongPress;

@property (nonatomic, strong) NSMutableDictionary *pins;
//@property (nonatomic, strong) NSMutableDictionary *pages;

@property (nonatomic, weak) UIToolbar *toolbar;
@property (nonatomic, weak) UINavigationBar *navigationBar;
@property (nonatomic, weak) UIView *navigationBarSuperView;

@end


@implementation CruiserWebViewController

- (instancetype)init
{
    self = [super init];

    if (self) {
        [self commonInit];
    }
    return self;
}

- (instancetype)initWithURL:(NSURL *)URL
{
    NSParameterAssert(URL);

    self = [self init];

    if (self) {
        _URL = URL;
    }
    return self;
}

- (instancetype)initWithFileURL:(NSURL *)URL
{
    // TODO: check is url valid and file exist
    return [self initWithURL:URL];
}

- (void)awakeFromNib
{
    [self commonInit];
}

- (void)commonInit
{
    self.currentSearchService        = CruiserSearchServicePrimary;
    self.primarySearchService        = kGoogleServiceRequestPath;
    self.alternativeSearchService    = kDuckDuckGoServiceRequestPath;
    self.supportedWebNavigationTools = CruiserWebNavigationToolAll;
    self.supportedWebActions         = CruiserWebActionAll;
    self.showLoadingProgress         = YES;
    self.hideBarsWithGestures        = YES;
    self.allowHistory                = YES;
    self.pins                        = [NSMutableDictionary dictionaryWithCapacity:1];
}


#pragma mark - View life cycle

- (void)loadView
{
    [super loadView];

    self.view = self.webView;
//    self.automaticallyAdjustsScrollViewInsets = YES;
}

- (void)viewDidLoad
{
    [super viewDidLoad];

    // IPNavBarSqueezableViewController
    self.triggeringScrollView = self.webView.scrollView;

    if (self.addressField) {
        self.titleFont = self.addressField.font;
    }
    __weak typeof(self) this = self;

    self.expandCompletion = ^{
        if (!this.addressField) {
            return;
        }
        this.addressField.backgroundColor = [this.addressField.backgroundColor
                                             colorWithAlphaComponent:1.f];
        this.addressField.borderStyle = UITextBorderStyleRoundedRect;
        this.addressField.rightViewMode = UITextFieldViewModeUnlessEditing;
        this.addressField.leftViewMode  = UITextFieldViewModeAlways;
    };
    self.downBarItem.enabled = NO;
    self.upBarItem.enabled   = NO;
}

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];

    [UIView performWithoutAnimation:^{
        static dispatch_once_t willAppearConfig;
        dispatch_once(&willAppearConfig, ^{
            [self configureToolBars];
        });
    }];

    if (!self.webView.URL
        && !self.webView.loading
        && self.URL
        ) {
        [self loadURL:self.URL];
    }
}

// TODO: speed up UI, not use Did Appear method
- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];

    static dispatch_once_t didAppearConfig;
    dispatch_once(&didAppearConfig, ^{
        [self configureBarItemsGestures];
    });
}

- (void)viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];

    [self clearProgressViewAnimated:animated];
}

- (void)viewDidDisappear:(BOOL)animated
{
    [super viewDidDisappear:animated];

    [self.webView stopLoading];
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // TODO: reduce memory usage
}

- (void)dealloc
{
    [self.navigationBar removeObserver:self
                            forKeyPath:@"hidden"
                               context:&CruiserWebViewControllerKVOContext];
    [self.navigationBar removeObserver:self
                            forKeyPath:@"center"
                               context:&CruiserWebViewControllerKVOContext];
    [self.navigationBar removeObserver:self
                            forKeyPath:@"alpha"
                               context:&CruiserWebViewControllerKVOContext];
}


#pragma mark - Public methods

- (void)loadPins
{
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];

    NSDictionary *loadedPins = [defaults dictionaryForKey:kPinsDictionaryKey];

    if (loadedPins) {
        self.pins = [loadedPins mutableCopy];
    }
}

- (void)storePins
{
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];

    [defaults setValue:self.pins
                forKey:kPinsDictionaryKey];
    [defaults synchronize];
}

#pragma mark - Getter methods

- (CruiserWebView *)webView
{
    if (!_webView) {
        CruiserWebView *webView = [[CruiserWebView alloc] initWithFrame:self.view.bounds
                                                          configuration:[WKWebViewConfiguration new]];
        webView.backgroundColor = [UIColor whiteColor];
        webView.allowsBackForwardNavigationGestures = YES;
        webView.UIDelegate = self;
        webView.navDelegate = self;
        webView.scrollView.delegate = self;

        _webView = webView;
    }
    return _webView;
}

- (UIProgressView *)progressView
{
    if (!_progressView) {
        CGFloat lineHeight = 2.f;
        CGRect frame = CGRectMake(0.f,
                                  CGRectGetHeight(self.navigationBar.bounds) - lineHeight,
                                  CGRectGetWidth(self.navigationBar.bounds),
                                  lineHeight);
        UIProgressView *progressView = [[UIProgressView alloc] initWithFrame:frame];
        progressView.trackTintColor = [UIColor clearColor];
        progressView.alpha = 0.f;

        [self.navigationBar addSubview:progressView];

        _progressView = progressView;
    }
    return _progressView;
}

- (UIBarButtonItem *)backwardBarItem
{
    if (!_backwardBarItem) {
        _backwardBarItem = [[UIBarButtonItem alloc] initWithImage:[self backwardButtonImage]
                                              landscapeImagePhone:nil
                                                            style:UIBarButtonItemStylePlain
                                                           target:self
                                                           action:@selector(goBackward:)];
        _backwardBarItem.accessibilityLabel = NSLocalizedStringFromTable(@"Backward",
                                                                         @"CruiserWebViewController",
                                                                         @"Accessibility label button title");
        _backwardBarItem.enabled = NO;
    }
    return _backwardBarItem;
}

- (UIBarButtonItem *)forwardBarItem
{
    if (!_forwardBarItem) {
        _forwardBarItem = [[UIBarButtonItem alloc] initWithImage:[self forwardButtonImage]
                                             landscapeImagePhone:nil
                                                           style:UIBarButtonItemStylePlain
                                                          target:self
                                                          action:@selector(goForward:)];
        _forwardBarItem.accessibilityLabel = NSLocalizedStringFromTable(@"Forward",
                                                                        @"CruiserWebViewController",
                                                                        @"Accessibility label button title");
        _forwardBarItem.enabled = NO;
    }
    return _forwardBarItem;
}

- (UIBarButtonItem *)pinBarItem
{
    if (!_pinBarItem) {
        _pinBarItem = [[UIBarButtonItem alloc] initWithImage:[self pinButtonImage]
                                         landscapeImagePhone:nil
                                                       style:UIBarButtonItemStylePlain
                                                      target:self
                                                      action:@selector(pinHere:)];
        _pinBarItem.accessibilityLabel = NSLocalizedStringFromTable(@"Pin",
                                                                    @"CruiserWebViewController",
                                                                    @"Accessibility label button title");
        _pinBarItem.enabled = NO;
    }
    return _pinBarItem;
}

- (UIBarButtonItem *)downBarItem
{
    if (!_downBarItem) {
        _downBarItem = [[UIBarButtonItem alloc] initWithImage:[self downButtonImage]
                                          landscapeImagePhone:nil
                                                        style:UIBarButtonItemStylePlain
                                                       target:self
                                                       action:@selector(scrollDown:)];
        _downBarItem.accessibilityLabel = NSLocalizedStringFromTable(@"Down",
                                                                     @"CruiserWebViewController",
                                                                     @"Accessibility label button title");
        _downBarItem.enabled = NO;
    }
    return _downBarItem;
}

- (UIBarButtonItem *)upBarItem
{
    if (!_upBarItem) {
        _upBarItem = [[UIBarButtonItem alloc] initWithImage:[self upButtonImage]
                                        landscapeImagePhone:nil
                                                      style:UIBarButtonItemStylePlain
                                                     target:self
                                                     action:@selector(scrollUp:)];
        _upBarItem.accessibilityLabel = NSLocalizedStringFromTable(@"Up",
                                                                   @"CruiserWebViewController",
                                                                   @"Accessibility label button title");
        _upBarItem.enabled = NO;
    }
    return _upBarItem;
}

- (UIBarButtonItem *)actionBarItem
{
    if (!_actionBarItem) {
        _actionBarItem = [[UIBarButtonItem alloc] initWithImage:[self actionButtonImage]
                                            landscapeImagePhone:nil
                                                          style:UIBarButtonItemStylePlain
                                                         target:self
                                                         action:@selector(presentActivityController:)];
        _actionBarItem.enabled = NO;
    }
    return _actionBarItem;
}

- (NSArray *)navigationToolItems
{
    NSMutableArray *items = [NSMutableArray new];

    UIBarButtonItem *flexibleSpace = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace
                                                                                   target:nil
                                                                                   action:NULL];
    if ((self.supportedWebNavigationTools & CruiserWebNavigationToolBackward) > 0 || self.supportsAllNavigationTools) {
        [items addObject:self.backwardBarItem];
    }
    if ((self.supportedWebNavigationTools & CruiserWebNavigationToolForward) > 0 || self.supportsAllNavigationTools) {
        if (!CRUISER_IS_IPAD) [items addObject:flexibleSpace];
        [items addObject:self.forwardBarItem];
    }
    if ((self.supportedWebNavigationTools & CruiserWebNavigationToolPin) > 0 || self.supportsAllNavigationTools) {
        if (!CRUISER_IS_IPAD) [items addObject:flexibleSpace];
        [items addObject:self.pinBarItem];
    }
    if ((self.supportedWebNavigationTools & CruiserWebNavigationToolDown) > 0 || self.supportsAllNavigationTools) {
        if (!CRUISER_IS_IPAD) [items addObject:flexibleSpace];
        [items addObject:self.downBarItem];
    }
    if ((self.supportedWebNavigationTools & CruiserWebNavigationToolUp) > 0 || self.supportsAllNavigationTools) {
        if (!CRUISER_IS_IPAD) [items addObject:flexibleSpace];
        [items addObject:self.upBarItem];
    }
    if (self.supportedWebActions > 0) {
        if (!CRUISER_IS_IPAD) [items addObject:flexibleSpace];
        [items addObject:self.actionBarItem];
    }
    return items;
}

- (BOOL)supportsAllNavigationTools
{
    return _supportedWebNavigationTools == CruiserWebNavigationToolAll;
}

- (UIButton *)stateButton
{
    if (!_stateButton) {
        UIImage *stateImage = [self reloadButtonImage];
        _stateButton = [UIButton buttonWithType:UIButtonTypeCustom];
        _stateButton.frame = CGRectMake(0.f,
                                        0.f,
                                        stateImage.size.width,
                                        stateImage.size.height);
        [_stateButton setImage:stateImage
                      forState:UIControlStateNormal];
        [self updateStateButton];
    }
    return _stateButton;
}

- (UIButton *)webButton
{
    if (!_webButton) {
        UIImage *webImage = [self primarySearchButtonImage];
        _webButton = [UIButton buttonWithType:UIButtonTypeCustom];
        _webButton.frame = CGRectMake(0.f,
                                      0.f,
                                      webImage.size.width,
                                      webImage.size.height);
        [_webButton setImage:webImage
                    forState:UIControlStateNormal];
        [_webButton addTarget:self
                       action:@selector(switchSearchService:)
             forControlEvents:UIControlEventTouchDown];
    }
    return _webButton;
}

- (UIImage *)backwardButtonImage
{
    if (!_backwardButtonImage) {
        _backwardButtonImage = [UIImage imageNamed:@"cruiser_icn_toolbar_backward"];
    }
    return _backwardButtonImage;
}

- (UIImage *)forwardButtonImage
{
    if (!_forwardButtonImage) {
        _forwardButtonImage = [UIImage imageNamed:@"cruiser_icn_toolbar_forward"];
    }
    return _forwardButtonImage;
}

- (UIImage *)pinButtonImage
{
    if (!_pinButtonImage) {
        _pinButtonImage = [UIImage imageNamed:@"cruiser_icn_toolbar_pin"];
    }
    return _pinButtonImage;
}

- (UIImage *)downButtonImage
{
    if (!_downButtonImage) {
        _downButtonImage = [UIImage imageNamed:@"cruiser_icn_toolbar_down"];
    }
    return _downButtonImage;
}

- (UIImage *)upButtonImage
{
    if (!_upButtonImage) {
        _upButtonImage = [UIImage imageNamed:@"cruiser_icn_toolbar_up"];
    }
    return _upButtonImage;
}

- (UIImage *)actionButtonImage
{
    if (!_actionButtonImage) {
        _actionButtonImage = [UIImage imageNamed:@"cruiser_icn_toolbar_action"];
    }
    return _actionButtonImage;
}

- (UIImage *)reloadButtonImage
{
    if (!_reloadButtonImage) {
        _reloadButtonImage = [UIImage imageNamed:@"cruiser_icn_toolbar_reload"];
    }
    return _reloadButtonImage;
}

- (UIImage *)stopButtonImage
{
    if (!_stopButtonImage) {
        _stopButtonImage = [UIImage imageNamed:@"cruiser_icn_toolbar_stop"];
    }
    return _stopButtonImage;
}

- (UIImage *)primarySearchButtonImage
{
    if (!_primarySearchButtonImage) {
        _primarySearchButtonImage = [UIImage imageNamed:@"cruiser_icn_toolbar_web"];
    }
    return _primarySearchButtonImage;
}

- (UIImage *)alternativeSearchButtonImage
{
    if (!_alternativeSearchButtonImage) {
        _alternativeSearchButtonImage = [UIImage imageNamed:@"cruiser_icn_toolbar_face"];
    }
    return _alternativeSearchButtonImage;
}

- (NSArray *)applicationActivitiesForItem:(id)item
{
    NSMutableArray *activities = [[NSMutableArray alloc]
                                  initWithCapacity:5];
    if ([item isKindOfClass:[UIImage class]]) {
        return activities;
    }
    if ((_supportedWebActions & CruiserWebActionCopyLink) > 0 || self.supportsAllActions) {
        [activities addObject:[CruiserPolyActivity activityWithType:CruiserPolyActivityTypeLink]];
    }
    if ((_supportedWebActions & CruiserWebActionOpenSafari) > 0 || self.supportsAllActions) {
        [activities addObject:[CruiserPolyActivity activityWithType:CruiserPolyActivityTypeSafari]];
    }
    if ((_supportedWebActions & CruiserWebActionOpenChrome) > 0 || self.supportsAllActions) {
        [activities addObject:[CruiserPolyActivity activityWithType:CruiserPolyActivityTypeChrome]];
    }
    if ((_supportedWebActions & CruiserWebActionOpenOperaMini) > 0 || self.supportsAllActions) {
        [activities addObject:[CruiserPolyActivity activityWithType:CruiserPolyActivityTypeOpera]];
    }
    if ((_supportedWebActions & CruiserWebActionOpenDolphin) > 0 || self.supportsAllActions) {
        [activities addObject:[CruiserPolyActivity activityWithType:CruiserPolyActivityTypeDolphin]];
    }
    return activities;
}

- (NSArray *)excludedActivityTypesForItem:(id)item
{
    NSMutableArray *types = [[NSMutableArray alloc]
                             initWithCapacity:10];
    if (![item isKindOfClass:[UIImage class]]) {
        [types addObjectsFromArray:@[UIActivityTypeCopyToPasteboard,
                                     UIActivityTypeSaveToCameraRoll,
                                     UIActivityTypePostToFlickr,
                                     UIActivityTypePrint,
                                     UIActivityTypeAssignToContact]];
    }
    if (self.supportsAllActions) {
        return types;
    }
    if ((_supportedWebActions & CruiserSupportedWebActionshareLink) == 0) {
        [types addObjectsFromArray:@[UIActivityTypeMail, UIActivityTypeMessage,
                                     UIActivityTypePostToFacebook, UIActivityTypePostToTwitter,
                                     UIActivityTypePostToWeibo, UIActivityTypePostToTencentWeibo,
                                     UIActivityTypeAirDrop]];
    }
    if ((_supportedWebActions & CruiserWebActionReadLater) == 0
        && [item isKindOfClass:[UIImage class]]
        ) {
        [types addObject:UIActivityTypeAddToReadingList];
    }
    return types;
}

- (BOOL)supportsAllActions
{
    return _supportedWebActions == CruiserWebActionAll;
}


#pragma mark - Setter methods

- (void)setURL:(NSURL *)URL
{
    if ([self.URL isEqual:URL]) {
        return;
    }
    if (self.isViewLoaded) {
        [self loadURL:URL];
    }
    _URL = URL;
}

// Sets the request errors with an alert view.
- (void)setLoadingError:(NSError *)error
{
    switch (error.code) {
        case NSURLErrorUnknown:
        case NSURLErrorCancelled:   return;
    }
    [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];

    [[[UIAlertView alloc] initWithTitle:NSLocalizedString(@"Error", nil)
                                message:error.localizedDescription
                               delegate:nil
                      cancelButtonTitle:NSLocalizedString(@"OK", nil)
                      otherButtonTitles:nil] show];
}

- (void)setAddressField:(UITextField *)addressField
{
    if (self.addressField == addressField) {
        return;
    }
    addressField.delegate = self;
    self->_addressField = addressField;
    self.titleFont = self.addressField.font;
}


#pragma mark - CruiserWebViewController methods

- (BOOL)validateHostname:(NSString *)query
{
    NSPredicate *urlTest = [NSPredicate predicateWithFormat:@"SELF MATCHES %@",
                            kHostnameRegex];
    return [urlTest evaluateWithObject:query];
}

- (NSURL *)URLFromString:(NSString *)query
{
    // try to use query as an URL
    NSURL *url = [NSURL URLWithString:query];

    if (url) {
        if (url.host && url.scheme) {
            return url;
        }
        if ([self validateHostname:query]) {
            url = [self URLFromString:
                   [NSString stringWithFormat:@"http://%@", query]];
            return url;
        }
    }
    // make search by query
    NSString *currentSearchServiceRequestPath = @"";

    switch (self.currentSearchService) {
        case CruiserSearchServicePrimary:
            currentSearchServiceRequestPath = [kGoogleServiceRequestPath copy];
            break;
        case CruiserSearchServiceAlternative:
            currentSearchServiceRequestPath = [kDuckDuckGoServiceRequestPath copy];
            break;
    }
    url = [NSURL URLWithString:[NSString stringWithFormat:@"%@%@",
                                currentSearchServiceRequestPath,
                                [query stringByAddingPercentEscapesUsingEncoding:
                                 NSASCIIStringEncoding]]];
    return url;
}

- (void)loadURL:(NSURL *)URL
{
    if ([URL isFileURL]) {
        NSData *data = [[NSData alloc] initWithContentsOfURL:URL];
        NSString *HTMLString = [[NSString alloc] initWithData:data
                                                     encoding:NSStringEncodingConversionAllowLossy];
        [self.webView loadHTMLString:HTMLString
                             baseURL:nil];
    } else {
        NSURLRequest *request = [[NSURLRequest alloc] initWithURL:URL];
        [self.webView loadRequest:request];
    }
}

- (void)goBackward:(id)sender
{
    if ([self.webView canGoBack]) {
        [self.webView goBack];
    }
}

- (void)goForward:(id)sender
{
    if ([self.webView canGoForward]) {
        [self.webView goForward];
    }
}

- (void)pinHere:(id)sender
{
    NSNumber *position = @(self.webView.scrollView.contentOffset.y);

    NSMutableArray *pagePins = self.pins[self.webView.URL.absoluteString];

    if (!pagePins) { // new page
        pagePins = [[NSMutableArray alloc] initWithCapacity:1];
        self.pins[self.webView.URL] = pagePins;
    }
    for (NSNumber *p in pagePins) {
        if ([p isEqualToNumber:position]) {
            return;
        }
    }
    [pagePins addObject:position];
    // sort pins for quick access
    [pagePins sortUsingComparator:^NSComparisonResult(id obj1, id obj2) {
        NSNumber *num1 = (NSNumber *)obj1;
        NSNumber *num2 = (NSNumber *)obj2;

        if (![num1 isKindOfClass:[NSNumber class]]
            || ![num2 isKindOfClass:[NSNumber class]]
            ) {
            return NSOrderedSame;
        }
        return [num1 compare:num2];
    }];
}

// TODO: speed up scrollDown: and scrollUp: method's enumeration with bisection method
- (void)scrollDown:(id)sender
{
    NSNumber *position = @(self.webView.scrollView.contentOffset.y);

    NSMutableArray *pagePins = self.pins[self.webView.URL.absoluteString];

    if (pagePins) {
        for (NSNumber *p in pagePins) {
            if ([p compare:position] == NSOrderedDescending) {
                CGPoint pinOffset = CGPointMake(0.f,
                                                p.floatValue);
                [self.webView.scrollView setContentOffset:pinOffset
                                                 animated:YES];
                return;
            }
        }
    }
    CGPoint bottomOffset = CGPointMake(0.f,
                                       self.webView.scrollView.contentSize.height
                                       - self.webView.scrollView.bounds.size.height);
    [self.webView.scrollView setContentOffset:bottomOffset
                                     animated:YES];
}

- (void)scrollUp:(id)sender
{
    NSNumber *position = @(self.webView.scrollView.contentOffset.y);

    NSMutableArray *pagePins = self.pins[self.webView.URL.absoluteString];

    if (pagePins) {
        for (NSNumber *p in [pagePins reverseObjectEnumerator]) {
            if ([p compare:position] == NSOrderedAscending) {
                CGPoint pinOffset = CGPointMake(0.f,
                                                p.floatValue);
                [self.webView.scrollView setContentOffset:pinOffset
                                                 animated:YES];
                return;
            }
        }
    }
    CGPoint topOffset = CGPointMake(0.f,
                                    -self.webView.scrollView.contentInset.top);
    [self.webView.scrollView setContentOffset:topOffset
                                     animated:YES];
}

- (void)dismissHistoryController
{
    if (self.presentedViewController) {
        [self.presentedViewController dismissViewControllerAnimated:YES completion:^{

            // The bar button item's gestures are invalidated after using them, so we must re-assign them.
            [self configureBarItemsGestures];
        }];
    }
}

- (void)showBackwardHistory:(UIGestureRecognizer *)sender
{
    if (!self.allowHistory || self.webView.backForwardList.backList.count == 0 || sender.state != UIGestureRecognizerStateBegan) {
        return;
    }

    [self presentHistoryControllerForTool:CruiserWebNavigationToolBackward fromView:sender.view];
}

- (void)showForwardHistory:(UIGestureRecognizer *)sender
{
    if (!self.allowHistory
        || self.webView.backForwardList.forwardList.count == 0
        || sender.state != UIGestureRecognizerStateBegan
        ) {
        return;
    }
    [self presentHistoryControllerForTool:CruiserWebNavigationToolForward fromView:sender.view];
}

- (void)presentHistoryControllerForTool:(CruiserWebNavigationTools)tool fromView:(UIView *)view
{
    UITableViewController *controller = [UITableViewController new];
    controller.title = NSLocalizedStringFromTable(@"History", @"CruiserWebViewController", nil);
    controller.tableView.delegate = self;
    controller.tableView.dataSource = self;
    controller.tableView.tag = tool;
    controller.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone
                                                                                                 target:self
                                                                                                 action:@selector(dismissHistoryController)];
    UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:controller];
    UIView *bar = CRUISER_IS_IPAD ? self.navigationBar : self.toolbar;

    if (CRUISER_IS_IPAD) {
        UIPopoverController *popover = [[UIPopoverController alloc] initWithContentViewController:navigationController];
        [popover presentPopoverFromRect:view.frame
                                 inView:bar
               permittedArrowDirections:UIPopoverArrowDirectionAny
                               animated:YES];
    } else {
        [self presentViewController:navigationController
                           animated:YES
                         completion:NULL];
    }
}

- (void)configureToolBars
{
    if (CRUISER_IS_IPAD) {
        self.navigationItem.rightBarButtonItems = [[[self navigationToolItems] reverseObjectEnumerator] allObjects];
    } else {
        [self setToolbarItems:[self navigationToolItems]];
    }
    self.toolbar = self.navigationController.toolbar;

    self.navigationBar = self.navigationController.navigationBar;
    self.navigationBarSuperView = self.navigationBar.superview;

    self.navigationController.hidesBarsWhenVerticallyCompact = self.hideBarsWithGestures;
    self.navigationController.hidesBarsOnSwipe               = NO; // because not good for navigation
    self.navigationController.hidesBottomBarWhenPushed       = NO; // because not good for navigation
    self.navigationController.hidesBarsWhenKeyboardAppears   = NO; // because of address field

    if (self.hideBarsWithGestures) {
        [self.navigationBar addObserver:self
                             forKeyPath:@"hidden"
                                options:NSKeyValueObservingOptionNew
                                context:&CruiserWebViewControllerKVOContext];
        [self.navigationBar addObserver:self
                             forKeyPath:@"center"
                                options:NSKeyValueObservingOptionNew
                                context:&CruiserWebViewControllerKVOContext];
        [self.navigationBar addObserver:self
                             forKeyPath:@"alpha"
                                options:NSKeyValueObservingOptionNew
                                context:&CruiserWebViewControllerKVOContext];
    }
    if (!CRUISER_IS_IPAD
        && self.navigationController.toolbarHidden
        && self.toolbarItems.count > 0
        ) {
        self.navigationController.toolbarHidden = NO;
    }
    if (self.addressField) {
        self.addressField.rightView = self.stateButton;
        self.addressField.leftView = self.webButton;
    }
}

// Light hack for adding custom gesture recognizers to UIBarButtonItems
- (void)configureBarItemsGestures
{
    UIView *backwardButton= [self.backwardBarItem valueForKey:@"view"];
    if (backwardButton.gestureRecognizers.count == 0) {
        if (!_backwardLongPress) {
            _backwardLongPress = [[UILongPressGestureRecognizer
                                   alloc] initWithTarget:self
                                                  action:@selector(showBackwardHistory:)];
        }
        [backwardButton addGestureRecognizer:self.backwardLongPress];
    }

    UIView *forwardBarButton= [self.forwardBarItem valueForKey:@"view"];
    if (forwardBarButton.gestureRecognizers.count == 0) {
        if (!_forwardLongPress) {
            _forwardLongPress = [[UILongPressGestureRecognizer
                                  alloc] initWithTarget:self
                                                 action:@selector(showForwardHistory:)];
        }
        [forwardBarButton addGestureRecognizer:self.forwardLongPress];
    }
}

- (void)updateToolbarItems
{
    [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:self.webView.loading];

    self.backwardBarItem.enabled = [self.webView canGoBack];
    self.forwardBarItem.enabled = [self.webView canGoForward];
    
    [self updatePinBarItems];

    self.actionBarItem.enabled = !self.webView.isLoading;

    [self updateStateButton];
    [self updateDownUpItems];
}

- (void)updatePinBarItems
{
    self.pinBarItem.enabled  = YES;
    self.downBarItem.enabled = YES;
    self.upBarItem.enabled   = YES;
}

- (void)updateStateButton
{
    [self.stateButton removeTarget:nil
                            action:NULL
                  forControlEvents:UIControlEventAllEvents];
    [self.stateButton addTarget:self.webView
                         action:self.webView.isLoading ? @selector(stopLoading) : @selector(reload)
               forControlEvents:UIControlEventTouchUpInside];
    [self.stateButton setImage:self.webView.isLoading ? self.stopButtonImage : self.reloadButtonImage
                      forState:UIControlStateNormal];
    self.stateButton.accessibilityLabel = NSLocalizedStringFromTable(self.webView.isLoading ? @"Stop"
                                                                     : @"Reload",
                                                                     @"CruiserWebViewController",
                                                                     @"Accessibility label button title");
    self.stateButton.enabled = YES;
}

- (void)switchSearchService:(id)sender
{
    switch (self.currentSearchService) {
        case CruiserSearchServicePrimary: {
            [self.webButton setImage:[self alternativeSearchButtonImage]
                            forState:UIControlStateNormal];
            self.currentSearchService = CruiserSearchServiceAlternative;
        }
            break;
        case CruiserSearchServiceAlternative: {
            [self.webButton setImage:[self primarySearchButtonImage]
                            forState:UIControlStateNormal];
            self.currentSearchService = CruiserSearchServicePrimary;
        }
            break;
    }
}

- (void)presentActivityController:(id)sender
{
    if (!self.webView.URL.absoluteString) {
        return;
    }

    [self presentActivityControllerWithItem:self.webView.URL.absoluteString
                                   andTitle:self.webView.title
                                     sender:sender];
}

- (void)presentActivityControllerWithItem:(id)item
                                 andTitle:(NSString *)title
                                   sender:(id)sender
{
    if (!item) {
        return;
    }
    UIActivityViewController *controller =
    [[UIActivityViewController alloc] initWithActivityItems:@[title, item]
                                      applicationActivities:[self applicationActivitiesForItem:item]];
    controller.excludedActivityTypes = [self excludedActivityTypesForItem:item];

    if (title) {
        [controller setValue:title forKey:@"subject"];
    }
    if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
        controller.popoverPresentationController.barButtonItem = sender;
    }
    [self presentViewController:controller
                       animated:YES
                     completion:NULL];
}

- (void)clearProgressViewAnimated:(BOOL)animated
{
    if (!_progressView) {
        return;
    }
    [UIView animateWithDuration:animated ? kAnimationDuration : 0.0
                     animations:^{
                         self.progressView.alpha = 0;
                     } completion:^(BOOL finished) {
                         [self destroyProgressViewIfNeeded];
                     }];
}

- (void)destroyProgressViewIfNeeded
{
    if (_progressView) {
        [_progressView removeFromSuperview];
        _progressView = nil;
    }
}

- (void)updateDownUpItems
{
    self.downBarItem.enabled = self.webView.scrollView.contentOffset.y + self.webView.scrollView.frame.size.height
                             < self.webView.scrollView.contentSize.height;
    self.upBarItem.enabled   = self.webView.scrollView.contentOffset.y > 0.f;
}


#pragma mark - CruiserNavigationDelegate methods

- (void)                webView:(CruiserWebView *)webView
  didStartProvisionalNavigation:(WKNavigation *)navigation
{
    [self updateStateButton];

    if (self.addressField) {
        self.addressField.text = self.webView.URL.absoluteString;
    }
    self.title = self.webView.URL.absoluteString;
}

- (void)      webView:(CruiserWebView *)webView
  didCommitNavigation:(WKNavigation *)navigation
{
    UIApplication.sharedApplication.networkActivityIndicatorVisible = self.webView.loading;
}

- (void)    webView:(CruiserWebView *)webView
  didUpdateProgress:(CGFloat)progress
{
    if (!self.showLoadingProgress) {
        [self destroyProgressViewIfNeeded];
        return;
    }
    if (self.progressView.alpha == 0.f && progress > 0) {

        self.progressView.progress = 0.f;

        [UIView animateWithDuration:0.2 animations:^{
            self.progressView.alpha = 1.f;
        }];
    }
    else if (self.progressView.alpha == 1.f && progress == 1.f)
    {
        [UIView animateWithDuration:0.2 animations:^{
            self.progressView.alpha = 0.f;
        } completion:^(BOOL finished) {
            self.progressView.progress = 0.f;
        }];
    }
    [self.progressView setProgress:progress
                          animated:YES];
}

- (void)      webView:(CruiserWebView *)webView
  didFinishNavigation:(WKNavigation *)navigation
{
    [self updateToolbarItems];

    if (self.addressField) {
        self.addressField.text = self.webView.title;
    }
    self.title = self.webView.title;
}

- (void)    webView:(CruiserWebView *)webView
  didFailNavigation:(WKNavigation *)navigation
          withError:(NSError *)error
{
    [self updateToolbarItems];
    [self setLoadingError:error];

    if (self.addressField) {
        self.addressField.text = @"";
    } else {
        self.title = @"";
    }
}


#pragma mark - WKUIDelegate methods

- (CruiserWebView *)     webView:(CruiserWebView *)webView
  createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration
             forNavigationAction:(WKNavigationAction *)navigationAction
                  windowFeatures:(WKWindowFeatures *)windowFeatures
{
    if (!navigationAction.targetFrame.isMainFrame) {
        [webView loadRequest:navigationAction.request];
    }
    return nil;
}

#pragma mark - UIScrollViewDelegate methods

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    [super scrollViewDidScroll:scrollView];
    [self updateDownUpItems];
}
/*
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView
                  willDecelerate:(BOOL)decelerate
{
    if (decelerate) {
        return;
    }
    if (self.toolbar.hidden || self.navigationBar.hidden) {
        [[NSNotificationCenter defaultCenter] postNotificationName:@"BarsShouldUnhide"
                                                            object:self];
    }
}

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
    if (self.toolbar.hidden || self.navigationBar.hidden) {
        [[NSNotificationCenter defaultCenter] postNotificationName:@"BarsShouldUnhide"
                                                            object:self];
    }
}

- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
    if (self.hideBarsWithGestures) {
        [[NSNotificationCenter defaultCenter] postNotificationName:@"BarsShouldHide"
                                                            object:self];
    }
}
*/

#pragma mark - IPNavBarSqueezableViewController

- (void)processBars
{
    [self squeezeAddressField];

    [super processBars];
}

- (void)squeezeBars
{
    [self squeezeAddressField];

    [super squeezeBars];
}

- (void)squeezeAddressField
{
    if (!self.addressField) {
        return;
    }
    self.addressField.text = self.webView.title;
    self.addressField.borderStyle = UITextBorderStyleNone;
    self.addressField.backgroundColor = [self.addressField.backgroundColor
                                         colorWithAlphaComponent:0.f];
    [self.addressField resignFirstResponder];

    self.addressField.rightViewMode = UITextFieldViewModeNever;
    self.addressField.leftViewMode  = UITextFieldViewModeNever;
}

#pragma mark - UITableViewDataSource Methods

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return 1;
}

- (NSInteger) tableView:(UITableView *)tableView
  numberOfRowsInSection:(NSInteger)section
{
    if (tableView.tag == CruiserWebNavigationToolBackward) {
        return self.webView.backForwardList.backList.count;
    }
    if (tableView.tag == CruiserWebNavigationToolForward) {
        return self.webView.backForwardList.forwardList.count;
    }
    return 0;
}

- (UITableViewCell *)tableView:(UITableView *)tableView
         cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];

    if (!cell) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle
                                      reuseIdentifier:CellIdentifier];
    }
    WKBackForwardListItem *item = nil;

    if (tableView.tag == CruiserWebNavigationToolBackward) {
        item = self.webView.backForwardList.backList[indexPath.row];
    }
    if (tableView.tag == CruiserWebNavigationToolForward) {
        item = self.webView.backForwardList.forwardList[indexPath.row];
    }
    cell.textLabel.text = item.title;
    cell.detailTextLabel.text = [item.URL absoluteString];

    return cell;
}

- (CGFloat)     tableView:(UITableView *)tableView
  heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    return 44.f;
}


#pragma mark - UITableViewDelegate Methods

- (void)        tableView:(UITableView *)tableView
  didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    WKBackForwardListItem *item = nil;

    if (tableView.tag == CruiserWebNavigationToolBackward) {
        item = self.webView.backForwardList.backList[indexPath.row];
    }
    if (tableView.tag == CruiserWebNavigationToolForward) {
        item = self.webView.backForwardList.forwardList[indexPath.row];
    }
    [self.webView goToBackForwardListItem:item];
    [self dismissHistoryController];
}


#pragma mark - Key Value Observer

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
    if (context != &CruiserWebViewControllerKVOContext) {
        [super observeValueForKeyPath:keyPath
                             ofObject:object
                               change:change
                              context:context];
        return;
    }
    if (![object isEqual:self.navigationBar]) {
        return;
    }
    // Skips for landscape orientation, since there is no status bar visible on iPhone landscape
    if (CRUISER_IS_LANDSCAPE) {
        return;
    }
    id newValue = change[NSKeyValueChangeNewKey];
    
    if ([keyPath isEqualToString:@"hidden"]
        && [newValue boolValue]
        && self.navigationBar.center.y >= -2.f
        ) {
        self.navigationBar.hidden = NO;
        
        if (!self.navigationBar.superview) {
            [self.navigationBarSuperView addSubview:self.navigationBar];
        }
    }
    if ([keyPath isEqualToString:@"center"]) {
        CGPoint center = [newValue CGPointValue];
        
        if (center.y < -2.f) {
            center.y = -2.f;
            self.navigationBar.center = center;
            
            [UIView beginAnimations:@"CruiserNavigationBarAnimation" context:nil];
            
            for (UIView *subview in self.navigationBar.subviews) {
                if (subview != self.navigationBar.subviews[0]) {
                    subview.alpha = 0.f;
                }
            }
            [UIView commitAnimations];
        }
    }
}


#pragma mark - View Auto-Rotation

- (NSUInteger)supportedInterfaceOrientations
{
    return UIInterfaceOrientationMaskAll;
}

- (BOOL)shouldAutorotate
{
    return NO;
}


#pragma mark - UITextFieldDelegate

- (void)textFieldDidBeginEditing:(UITextField *)textField
{
    if (textField != self.addressField) {
        return;
    }
    textField.textAlignment = NSTextAlignmentLeft;
    textField.text = self.webView.URL.absoluteString;
    self.title = self.addressField.text;
}

- (void)textFieldDidEndEditing:(UITextField *)textField
{
    if (textField != self.addressField) {
        return;
    }
    textField.textAlignment = NSTextAlignmentCenter;
    textField.text = self.webView.title;
    self.title = self.addressField.text;
}

- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
    if (textField != self.addressField) {
        return YES;
    }
    [self loadURL:[self URLFromString:textField.text]];
    [textField resignFirstResponder];
    return NO;
}

@end