// // URLBar.swift // SBrowser // // Created by James Magahern on 7/23/20. // import UIKit 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 { didSet { updateProgressIndicator() } } private let backgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .systemThickMaterial)) private let shadowView = UIView(frame: .zero) private let progressIndicatorView = ProgressIndicatorView() private let fadeMaskView = UIImageView(frame: .zero) private let refreshImage = UIImage(systemName: "arrow.clockwise") private let stopImage = UIImage(systemName: "xmark") private let backgroundCornerRadius: CGFloat = 8 convenience init() { self.init(frame: .zero) backgroundColor = .clear backgroundView.layer.masksToBounds = true backgroundView.layer.cornerRadius = backgroundCornerRadius backgroundView.layer.borderWidth = 1 backgroundView.layer.borderColor = UIColor.secondarySystemFill.cgColor backgroundView.isUserInteractionEnabled = false addSubview(backgroundView) backgroundView.contentView.addSubview(progressIndicatorView) textField.backgroundColor = .clear textField.textContentType = .URL textField.keyboardType = .webSearch textField.autocorrectionType = .no textField.autocapitalizationType = .none textField.font = .systemFont(ofSize: 14.0) textField.clearingBehavior = .clearOnInsertionAndShowSelectionTint textField.clearButtonMode = .whileEditing textField.addAction(UIAction(handler: { [unowned self] _ in // Mask view visibility is affected by editing state. self.layoutSubviews() }), for: [ .editingDidBegin, .editingDidEnd ]) addSubview(textField) textField.addAction(.init(handler: { [textField, refreshButton] _ in refreshButton.isHidden = textField.isFirstResponder }), for: [ .editingDidBegin, .editingDidEnd ]) 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: self.refreshButton.setImage(self.refreshImage, for: .normal) self.progressIndicatorView.progress = 1.0 UIView.animate(withDuration: 0.5, delay: 0.5, options: AnimationOptions()) { self.progressIndicatorView.alpha = 0.0 } completion: { _ in // 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 { let preferredHeight = CGFloat(34) return CGSize(width: 1000.0, height: preferredHeight) } private func fadeBackgroundImageForSize(_ size: CGSize, cutoffLocation: CGFloat) -> UIImage? { var image: UIImage? = nil UIGraphicsBeginImageContext(CGSize(width: size.width, height: 1.0)) if let context = UIGraphicsGetCurrentContext() { let gradientColorsArray = [ UIColor(white: 1.0, alpha: 1.0).cgColor, UIColor(white: 1.0, alpha: 1.0).cgColor, UIColor(white: 1.0, alpha: 0.08).cgColor, UIColor(white: 1.0, alpha: 0.08).cgColor ] let locations: [CGFloat] = [ 0.0, cutoffLocation, cutoffLocation + 0.10, 1.0 ] if let gradient = CGGradient(colorsSpace: nil, colors: gradientColorsArray as CFArray, locations: locations) { context.drawLinearGradient(gradient, start: .zero, end: CGPoint(x: size.width, y: 0.0), options: CGGradientDrawingOptions()) } image = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext(); } 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.layer.add(blinkAnimation, forKey: animationKey) } else { self.errorButton.layer.removeAnimation(forKey: animationKey) } } override func layoutSubviews() { super.layoutSubviews() backgroundView.frame = bounds shadowView.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, !textField.isFirstResponder { errorButton.isHidden = false 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 } else { errorButton.isHidden = true } // Fade mask fadeMaskView.frame = textField.bounds fadeMaskView.image = fadeBackgroundImageForSize(fadeMaskView.frame.size, cutoffLocation: fadeCutoffLocation) if !textField.isFirstResponder { textField.mask = fadeMaskView } else { textField.mask = nil } } } class ProgressIndicatorView: UIView { public var progress: Double = 0.0 { didSet { layoutSubviews() } } private let progressFillView = UIView(frame: .zero) convenience init() { self.init(frame: .zero) progressFillView.backgroundColor = .systemBlue progressFillView.alpha = 0.3 addSubview(progressFillView) } override func layoutSubviews() { super.layoutSubviews() let width = CGFloat(progress) * bounds.width progressFillView.frame = CGRect(origin: .zero, size: CGSize(width: width, height: bounds.height)) } }