Skip to content

Instantly share code, notes, and snippets.

@elimence
Created September 20, 2017 00:09
Show Gist options
  • Save elimence/91cf35dfe677b38b23a27c6d54d44285 to your computer and use it in GitHub Desktop.
Save elimence/91cf35dfe677b38b23a27c6d54d44285 to your computer and use it in GitHub Desktop.
//
// GDWebViewController.swift
// GDWebBrowserClient
//
// Created by Alex G on 03.12.14.
// Copyright (c) 2015 Alexey Gordiyenko. All rights reserved.
//
//MIT License
//
//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.
import UIKit
import WebKit
public enum GDWebViewControllerProgressIndicatorStyle {
case activityIndicator
case progressView
case both
case none
}
@objc public protocol GDWebViewControllerDelegate {
@objc optional func webViewController(_ webViewController: GDWebViewController, didChangeURL newURL: URL?)
@objc optional func webViewController(_ webViewController: GDWebViewController, didChangeTitle newTitle: NSString?)
@objc optional func webViewController(_ webViewController: GDWebViewController, decidePolicyForNavigationAction navigationAction: WKNavigationAction, decisionHandler: (WKNavigationActionPolicy) -> Void)
@objc optional func webViewController(_ webViewController: GDWebViewController, decidePolicyForNavigationResponse navigationResponse: WKNavigationResponse, decisionHandler: (WKNavigationResponsePolicy) -> Void);
@objc optional func webViewController(_ webViewController: GDWebViewController, didReceiveAuthenticationChallenge challenge: URLAuthenticationChallenge, completionHandler: (URLSession.AuthChallengeDisposition, URLCredential?) -> Void);
}
open class GDWebViewController: UIViewController, WKNavigationDelegate, GDWebViewNavigationToolbarDelegate {
// MARK: Public Properties
/** An object to serve as a delegate which conforms to GDWebViewNavigationToolbarDelegate protocol. */
open weak var delegate: GDWebViewControllerDelegate?
/** The style of progress indication visualization. Can be one of four values: .ActivityIndicator, .ProgressView, .Both, .None*/
open var progressIndicatorStyle: GDWebViewControllerProgressIndicatorStyle = .both
/** A Boolean value indicating whether horizontal swipe gestures will trigger back-forward list navigations. The default value is false. */
open var allowsBackForwardNavigationGestures: Bool {
get {
return webView.allowsBackForwardNavigationGestures
}
set(value) {
webView.allowsBackForwardNavigationGestures = value
}
}
/** A boolean value if set to true shows the toolbar; otherwise, hides it. */
open var showsToolbar: Bool {
set(value) {
self.toolbarHeight = value ? 44 : 0
}
get {
return self.toolbarHeight == 44
}
}
/** A boolean value if set to true shows the refresh control (or stop control while loading) on the toolbar; otherwise, hides it. */
open var showsStopRefreshControl: Bool {
get {
return toolbarContainer.showsStopRefreshControl
}
set(value) {
toolbarContainer.showsStopRefreshControl = value
}
}
/** The navigation toolbar object (read-only). */
open var toolbar: GDWebViewNavigationToolbar {
get {
return toolbarContainer
}
}
// MARK: Private Properties
fileprivate var webView: WKWebView!
fileprivate var progressView: UIProgressView!
fileprivate var toolbarContainer: GDWebViewNavigationToolbar!
fileprivate var toolbarHeightConstraint: NSLayoutConstraint!
fileprivate var toolbarHeight: CGFloat = 0
fileprivate var navControllerUsesBackSwipe: Bool = false
lazy fileprivate var activityIndicator: UIActivityIndicatorView! = {
var activityIndicator = UIActivityIndicatorView()
activityIndicator.backgroundColor = UIColor(white: 0, alpha: 0.2)
activityIndicator.activityIndicatorViewStyle = .whiteLarge
activityIndicator.hidesWhenStopped = true
activityIndicator.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(activityIndicator)
self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "|-0-[activityIndicator]-0-|", options: [], metrics: nil, views: ["activityIndicator": activityIndicator]))
self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[topGuide]-0-[activityIndicator]-0-[toolbarContainer]|", options: [], metrics: nil, views: ["activityIndicator": activityIndicator, "toolbarContainer": self.toolbarContainer, "topGuide": self.topLayoutGuide]))
return activityIndicator
}()
// MARK: Public Methods
/**
Navigates to an URL created from provided string.
- parameter URLString: The string that represents an URL.
*/
// TODO: Earlier `scheme` property was optional. Now it isn't true. Need to check that scheme is always
open func loadURLWithString(_ URLString: String) {
if let URL = URL(string: URLString) {
if (URL.scheme != "") && (URL.host != nil) {
loadURL(URL)
} else {
loadURLWithString("http://\(URLString)")
}
}
}
/**
Navigates to the URL.
- parameter URL: The URL for a request.
- parameter cachePolicy: The cache policy for a request. Optional. Default value is .UseProtocolCachePolicy.
- parameter timeoutInterval: The timeout interval for a request, in seconds. Optional. Default value is 0.
*/
open func loadURL(_ URL: Foundation.URL, cachePolicy: URLRequest.CachePolicy = .useProtocolCachePolicy, timeoutInterval: TimeInterval = 0) {
webView.load(URLRequest(url: URL, cachePolicy: cachePolicy, timeoutInterval: timeoutInterval))
}
/**
Shows or hides toolbar.
- parameter show: A Boolean value if set to true shows the toolbar; otherwise, hides it.
- parameter animated: A Boolean value if set to true animates the transition; otherwise, does not.
*/
open func showToolbar(_ show: Bool, animated: Bool) {
self.showsToolbar = show
if toolbarHeightConstraint != nil {
toolbarHeightConstraint.constant = self.toolbarHeight
if animated {
UIView.animate(withDuration: 0.2, animations: { () -> Void in
self.view.layoutIfNeeded()
})
} else {
self.view.layoutIfNeeded()
}
}
}
// MARK: GDWebViewNavigationToolbarDelegate Methods
func webViewNavigationToolbarGoBack(_ toolbar: GDWebViewNavigationToolbar) {
webView.goBack()
}
func webViewNavigationToolbarGoForward(_ toolbar: GDWebViewNavigationToolbar) {
webView.goForward()
}
func webViewNavigationToolbarRefresh(_ toolbar: GDWebViewNavigationToolbar) {
webView.reload()
}
func webViewNavigationToolbarStop(_ toolbar: GDWebViewNavigationToolbar) {
webView.stopLoading()
}
// MARK: WKNavigationDelegate Methods
open func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
}
open func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
showLoading(false)
if error._code == NSURLErrorCancelled {
return
}
showError(error.localizedDescription)
}
open func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
showLoading(false)
if error._code == NSURLErrorCancelled {
return
}
showError(error.localizedDescription)
}
open func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
showLoading(false)
backForwardListChanged()
}
open func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
delegate?.webViewController?(self, didReceiveAuthenticationChallenge: challenge, completionHandler: { (disposition, credential) -> Void in
completionHandler(disposition, credential)
}) ?? completionHandler(.performDefaultHandling, nil)
}
open func webView(_ webView: WKWebView, didReceiveServerRedirectForProvisionalNavigation navigation: WKNavigation!) {
}
open func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
showLoading(true)
}
open func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
delegate?.webViewController?(self, decidePolicyForNavigationAction: navigationAction, decisionHandler: { (policy) -> Void in
decisionHandler(policy)
if policy == .cancel {
self.showError("This navigation is prohibited.")
}
}) ?? decisionHandler(WKNavigationActionPolicy.allow)
}
open func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) {
delegate?.webViewController?(self, decidePolicyForNavigationResponse: navigationResponse, decisionHandler: { (policy) -> Void in
decisionHandler(policy)
if policy == .cancel {
self.showError("This navigation response is prohibited.")
}
}) ?? decisionHandler(WKNavigationResponsePolicy.allow)
}
// MARK: Some Private Methods
fileprivate func showError(_ errorString: String?) {
let alertView = UIAlertController(title: "Error", message: errorString, preferredStyle: .alert)
alertView.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
self.present(alertView, animated: true, completion: nil)
}
fileprivate func showLoading(_ animate: Bool) {
if animate {
if (progressIndicatorStyle == .activityIndicator) || (progressIndicatorStyle == .both) {
activityIndicator.startAnimating()
}
toolbar.loadDidStart()
} else if activityIndicator != nil {
if (progressIndicatorStyle == .activityIndicator) || (progressIndicatorStyle == .both) {
activityIndicator.stopAnimating()
}
toolbar.loadDidFinish()
}
}
fileprivate func progressChanged(_ newValue: NSNumber) {
if progressView == nil {
progressView = UIProgressView()
progressView.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(progressView)
self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "|-0-[progressView]-0-|", options: [], metrics: nil, views: ["progressView": progressView]))
self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[topGuide]-0-[progressView(2)]", options: [], metrics: nil, views: ["progressView": progressView, "topGuide": self.topLayoutGuide]))
}
progressView.progress = newValue.floatValue
if progressView.progress == 1 {
progressView.progress = 0
UIView.animate(withDuration: 0.2, animations: { () -> Void in
self.progressView.alpha = 0
})
} else if progressView.alpha == 0 {
UIView.animate(withDuration: 0.2, animations: { () -> Void in
self.progressView.alpha = 1
})
}
}
fileprivate func backForwardListChanged() {
if self.navControllerUsesBackSwipe && self.allowsBackForwardNavigationGestures {
self.navigationController?.interactivePopGestureRecognizer?.isEnabled = !webView.canGoBack
}
toolbarContainer.backButtonItem?.isEnabled = webView.canGoBack
toolbarContainer.forwardButtonItem?.isEnabled = webView.canGoForward
}
// MARK: KVO
override open func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
guard let keyPath = keyPath else {return}
switch keyPath {
case "estimatedProgress":
if (progressIndicatorStyle == .progressView) || (progressIndicatorStyle == .both) {
if let newValue = change?[NSKeyValueChangeKey.newKey] as? NSNumber {
progressChanged(newValue)
}
}
case "URL":
delegate?.webViewController?(self, didChangeURL: webView.url)
case "title":
delegate?.webViewController?(self, didChangeTitle: webView.title as NSString?)
default:
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
}
}
// MARK: Life Cycle
override open func viewDidLoad() {
super.viewDidLoad()
// Set up toolbarContainer
self.view.addSubview(toolbarContainer)
self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "|-0-[toolbarContainer]-0-|", options: [], metrics: nil, views: ["toolbarContainer": toolbarContainer]))
self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:[toolbarContainer]-0-|", options: [], metrics: nil, views: ["toolbarContainer": toolbarContainer]))
toolbarHeightConstraint = NSLayoutConstraint(item: toolbarContainer, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: toolbarHeight)
toolbarContainer.addConstraint(toolbarHeightConstraint)
// Set up webView
self.view.addSubview(webView)
self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "|-0-[webView]-0-|", options: [], metrics: nil, views: ["webView": webView]))
self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[topGuide]-0-[webView]-0-[toolbarContainer]|", options: [], metrics: nil, views: ["webView": webView, "toolbarContainer": toolbarContainer, "topGuide": self.topLayoutGuide]))
}
override open func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
webView.addObserver(self, forKeyPath: "estimatedProgress", options: .new, context: nil)
webView.addObserver(self, forKeyPath: "URL", options: .new, context: nil)
webView.addObserver(self, forKeyPath: "title", options: .new, context: nil)
}
override open func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
webView.removeObserver(self, forKeyPath: "estimatedProgress")
webView.removeObserver(self, forKeyPath: "URL")
webView.removeObserver(self, forKeyPath: "title")
}
override open func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if let navVC = self.navigationController {
if let gestureRecognizer = navVC.interactivePopGestureRecognizer {
navControllerUsesBackSwipe = gestureRecognizer.isEnabled
} else {
navControllerUsesBackSwipe = false
}
}
}
override open func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
if navControllerUsesBackSwipe {
self.navigationController?.interactivePopGestureRecognizer?.isEnabled = true
}
}
override open func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
webView.stopLoading()
}
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
self.commonInit()
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.commonInit()
}
open func commonInit() {
webView = WKWebView()
webView.navigationDelegate = self
webView.translatesAutoresizingMaskIntoConstraints = false
toolbarContainer = GDWebViewNavigationToolbar(delegate: self)
toolbarContainer.translatesAutoresizingMaskIntoConstraints = false
}
}
//
// GDWebViewNavigationToolbar.swift
// GDWebBrowserClient
//
// Created by Alex G on 04.12.14.
// Copyright (c) 2015 Alexey Gordiyenko. All rights reserved.
//
//MIT License
//
//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.
import UIKit
@objc protocol GDWebViewNavigationToolbarDelegate {
func webViewNavigationToolbarGoBack(_ toolbar: GDWebViewNavigationToolbar)
func webViewNavigationToolbarGoForward(_ toolbar: GDWebViewNavigationToolbar)
func webViewNavigationToolbarRefresh(_ toolbar: GDWebViewNavigationToolbar)
func webViewNavigationToolbarStop(_ toolbar: GDWebViewNavigationToolbar)
}
open class GDWebViewNavigationToolbar: UIView {
// MARK: Public Properties
var toolbar: UIToolbar! {
get {
return _toolbar
}
}
weak var delegate: GDWebViewNavigationToolbarDelegate?
var backButtonItem: UIBarButtonItem? {
get {
return _backButtonItem
}
}
var forwardButtonItem: UIBarButtonItem? {
get {
return _forwardButtonItem
}
}
var refreshButtonItem: UIBarButtonItem? {
get {
return _refreshButtonItem
}
}
/** The tint color to apply to the toolbar button items.*/
var toolbarTintColor: UIColor? {
get {
return _toolbarTintColor
}
set(value) {
_toolbarTintColor = value
if let toolbar = self.toolbar {
toolbar.tintColor = _toolbarTintColor
}
}
}
/** The toolbar's background color.*/
var toolbarBackgroundColor: UIColor? {
get {
return _toolbarBackgroundColor
}
set(value) {
_toolbarBackgroundColor = value
if let toolbar = self.toolbar {
toolbar.backgroundColor = _toolbarBackgroundColor
}
}
}
/** A Boolean value that indicates whether the toolbar is translucent (true) or not (false).*/
var toolbarTranslucent: Bool {
get {
return _toolbarTranslucent
}
set(value) {
_toolbarTranslucent = value
if let toolbar = self.toolbar {
toolbar.isTranslucent = _toolbarTranslucent
}
}
}
var showsStopRefreshControl: Bool {
get {
return _showsStopRefreshControl
}
set(value) {
if _toolbar != nil {
if value && !_showsStopRefreshControl {
_toolbar.setItems([_backButtonItem, _forwardButtonItem, _flexibleSpace, _refreshButtonItem], animated: false)
} else if !value && _showsStopRefreshControl {
_toolbar.setItems([_backButtonItem, _forwardButtonItem], animated: false)
}
}
_showsStopRefreshControl = value
}
}
// MARK: Private Properties
fileprivate var _toolbar: UIToolbar!
fileprivate lazy var _backButtonItem: UIBarButtonItem = {
let backButtonItem = UIBarButtonItem(title: "\u{25C0}\u{FE0E}", style: UIBarButtonItemStyle.plain, target: self, action: #selector(GDWebViewNavigationToolbar.goBack))
backButtonItem.isEnabled = false
return backButtonItem
}()
fileprivate lazy var _forwardButtonItem: UIBarButtonItem = {
let forwardButtonItem = UIBarButtonItem(title: "\u{25B6}\u{FE0E}", style: UIBarButtonItemStyle.plain, target: self, action: #selector(GDWebViewNavigationToolbar.goForward))
forwardButtonItem.isEnabled = false
return forwardButtonItem
}()
fileprivate lazy var _refreshButtonItem: UIBarButtonItem = {UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.refresh, target: self, action: #selector(GDWebViewNavigationToolbar.refresh))}()
fileprivate lazy var _stopButtonItem: UIBarButtonItem = {UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.stop, target: self, action: #selector(GDWebViewNavigationToolbar.stop))}()
fileprivate lazy var _flexibleSpace: UIBarButtonItem = {UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.flexibleSpace, target: nil, action: nil)}()
fileprivate var _toolbarTintColor: UIColor?
fileprivate var _toolbarBackgroundColor: UIColor?
fileprivate var _toolbarTranslucent = true
fileprivate var _showsStopRefreshControl = true
// MARK: Public Methods
open func loadDidStart() {
if !_showsStopRefreshControl {
return
}
let items = [_backButtonItem, _forwardButtonItem, _flexibleSpace, _stopButtonItem]
_toolbar.setItems(items, animated: true)
}
open func loadDidFinish() {
if !_showsStopRefreshControl {
return
}
let items = [_backButtonItem, _forwardButtonItem, _flexibleSpace, _refreshButtonItem]
_toolbar.setItems(items, animated: true)
}
// MARK: Navigation Methods
func goBack() {
delegate?.webViewNavigationToolbarGoBack(self)
}
func goForward() {
delegate?.webViewNavigationToolbarGoForward(self)
}
func refresh() {
delegate?.webViewNavigationToolbarRefresh(self)
}
func stop() {
delegate?.webViewNavigationToolbarStop(self)
}
// MARK: Life Cycle
init(delegate: GDWebViewNavigationToolbarDelegate) {
super.init(frame: CGRect.zero)
self.delegate = delegate
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init(frame: CGRect) {
super.init(frame: frame)
}
override open func layoutSubviews() {
super.layoutSubviews()
if (_toolbar == nil) {
_toolbar = UIToolbar()
_toolbar.tintColor = _toolbarTintColor
_toolbar.backgroundColor = _toolbarBackgroundColor
_toolbar.isTranslucent = _toolbarTranslucent
_toolbar.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(_toolbar)
self.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "|-0-[toolbar]-0-|", options: [], metrics: nil, views: ["toolbar": _toolbar]))
self.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-0-[toolbar]-0-|", options: [], metrics: nil, views: ["toolbar": _toolbar]))
// Set up _toolbar
let items = _showsStopRefreshControl ? [_backButtonItem, _forwardButtonItem, _flexibleSpace, _refreshButtonItem] : [_backButtonItem, _forwardButtonItem]
_toolbar.setItems(items, animated: false)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment