diff --git a/App/Browser View/BrowserViewController.swift b/App/Browser View/BrowserViewController.swift index ec89990..609bd78 100644 --- a/App/Browser View/BrowserViewController.swift +++ b/App/Browser View/BrowserViewController.swift @@ -28,6 +28,8 @@ class BrowserViewController: UIViewController, WKNavigationDelegate, private var backButtonObservation: NSKeyValueObservation? private var forwardButtonObservation: NSKeyValueObservation? + private var loadError: Error? + override var preferredStatusBarStyle: UIStatusBarStyle { .lightContent } init() { @@ -139,6 +141,22 @@ class BrowserViewController: UIViewController, WKNavigationDelegate, // New tab button toolbarController.newTabButton.addAction(newTabAction, for: .touchUpInside) + // Error button + toolbarController.urlBar.errorButton.addAction(UIAction(handler: { [unowned self] _ in + let alert = UIAlertController(title: "Error", message: self.loadError?.localizedDescription, preferredStyle: .actionSheet) + + alert.addAction(UIAlertAction(title: "Reload", style: .destructive, handler: { _ in + self.webView.reload() + alert.dismiss(animated: true, completion: nil) + })) + + alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { _ in + alert.dismiss(animated: true, completion: nil) + })) + + self.present(alert, animated: true, completion: nil) + }), for: .touchUpInside) + // TextField delegate toolbarController.urlBar.textField.delegate = self @@ -146,7 +164,9 @@ class BrowserViewController: UIViewController, WKNavigationDelegate, } private func updateLoadProgress(forWebView webView: WKWebView) { - if webView.estimatedProgress == 1.0 { + if let loadError = loadError { + toolbarController.urlBar.loadProgress = .error(error: loadError) + } else if webView.estimatedProgress == 1.0 { toolbarController.urlBar.loadProgress = .complete } else { toolbarController.urlBar.loadProgress = .loading(progress: webView.estimatedProgress) @@ -243,6 +263,8 @@ class BrowserViewController: UIViewController, WKNavigationDelegate, // MARK: Navigation Delegate func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { + loadError = nil + // Reset tracking this tab.allowedScriptOrigins.removeAll() tab.blockedScriptOrigins.removeAll() @@ -272,6 +294,14 @@ class BrowserViewController: UIViewController, WKNavigationDelegate, decisionHandler(.allow, preferences) } + func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { + self.loadError = error + } + + func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { + self.loadError = error + } + // MARK: UITextField Delegate func textFieldShouldReturn(_ textField: UITextField) -> Bool { diff --git a/App/Titlebar and URL Bar/URLBar.swift b/App/Titlebar and URL Bar/URLBar.swift index fdc48a4..299aeba 100644 --- a/App/Titlebar and URL Bar/URLBar.swift +++ b/App/Titlebar and URL Bar/URLBar.swift @@ -11,10 +11,12 @@ class URLBar: UIView { let textField = UITextField(frame: .zero) let refreshButton = UIButton(frame: .zero) + let errorButton = UIButton(frame: .zero) public enum LoadProgress { case complete case loading(progress: Double) + case error(error: Error) } public var loadProgress: LoadProgress = .complete { @@ -65,9 +67,21 @@ class URLBar: UIView refreshButton.tintColor = .secondaryLabel refreshButton.setImage(refreshImage, for: .normal) addSubview(refreshButton) + + errorButton.backgroundColor = .systemRed + errorButton.layer.cornerRadius = 3.0 + errorButton.layer.masksToBounds = true + errorButton.titleLabel?.font = UIFont.boldSystemFont(ofSize: 11.0) + errorButton.setTitleColor(.white, for: .normal) + errorButton.setTitle("ERR", for: .normal) + addSubview(errorButton) + + setErrorButtonAnimating(false) } private func updateProgressIndicator() { + setErrorButtonAnimating(false) + UIView.animate(withDuration: 0.4) { switch self.loadProgress { case .complete: @@ -79,12 +93,26 @@ class URLBar: UIView // Reset back to zero self.progressIndicatorView.progress = 0.0 } + case .loading(let progress): self.refreshButton.setImage(self.stopImage, for: .normal) self.progressIndicatorView.progress = progress self.progressIndicatorView.alpha = 1.0 + + case .error(let error): + self.setErrorButtonAnimating(true) + self.progressIndicatorView.alpha = 0.0 + self.progressIndicatorView.progress = 0.0 + + if let nserror = error as NSError? { + self.errorButton.setTitle("\(nserror.code)", for: .normal) + } else { + self.errorButton.setTitle("ERR", for: .normal) + } } } + + self.setNeedsLayout() } override var intrinsicContentSize: CGSize { @@ -92,7 +120,7 @@ class URLBar: UIView return CGSize(width: 1000.0, height: preferredHeight) } - private func fadeBackgroundImageForSize(_ size: CGSize) -> UIImage? { + private func fadeBackgroundImageForSize(_ size: CGSize, cutoffLocation: CGFloat) -> UIImage? { var image: UIImage? = nil UIGraphicsBeginImageContext(CGSize(width: size.width, height: 1.0)) @@ -105,7 +133,7 @@ class URLBar: UIView ] let locations: [CGFloat] = [ - 0.0, 0.80, 0.90, 1.0 + 0.0, cutoffLocation, cutoffLocation + 0.10, 1.0 ] if let gradient = CGGradient(colorsSpace: nil, colors: gradientColorsArray as CFArray, locations: locations) { @@ -119,22 +147,57 @@ class URLBar: UIView return image } + private func setErrorButtonAnimating(_ animating: Bool) { + let animationKey = "blinkAnimation" + if animating { + let blinkAnimation = CAKeyframeAnimation(keyPath: "opacity") + blinkAnimation.calculationMode = .discrete + blinkAnimation.values = [ 1.0, 0.1, 1.0 ] + blinkAnimation.keyTimes = [ 0.0, 0.5, 1.0 ] + blinkAnimation.duration = 1.0 + blinkAnimation.repeatCount = .infinity + + self.errorButton.isHidden = false + self.errorButton.layer.add(blinkAnimation, forKey: animationKey) + } else { + self.errorButton.isHidden = true + self.errorButton.layer.removeAnimation(forKey: animationKey) + } + } + override func layoutSubviews() { super.layoutSubviews() backgroundView.frame = bounds progressIndicatorView.frame = backgroundView.contentView.bounds textField.frame = bounds.insetBy(dx: 6.0, dy: 0) + var fadeCutoffLocation: CGFloat = 0.8 + + // Refresh button + let refreshButtonSize = CGSize(width: textField.frame.height, height: textField.frame.height) + refreshButton.frame = CGRect(origin: CGPoint(x: bounds.width - refreshButtonSize.width, y: 0), size: refreshButtonSize) + + // Error button + if case .error(error: _) = loadProgress { + errorButton.sizeToFit() + errorButton.frame = CGRect( + x: refreshButton.frame.minX - errorButton.frame.width - 8.0, + y: 0.0, + width: errorButton.frame.width + 8.0, + height: 22.0 + ) + errorButton.frame = errorButton.frame.centeredY(inRect: bounds) + fadeCutoffLocation = (errorButton.frame.minX / bounds.width) - 0.1 + } + + // Fade mask fadeMaskView.frame = textField.bounds - fadeMaskView.image = fadeBackgroundImageForSize(fadeMaskView.frame.size) + fadeMaskView.image = fadeBackgroundImageForSize(fadeMaskView.frame.size, cutoffLocation: fadeCutoffLocation) if !textField.isFirstResponder { textField.mask = fadeMaskView } else { textField.mask = nil } - - let refreshButtonSize = CGSize(width: textField.frame.height, height: textField.frame.height) - refreshButton.frame = CGRect(origin: CGPoint(x: bounds.width - refreshButtonSize.width, y: 0), size: refreshButtonSize) } }