Files
Attractor/App/Titlebar and URL Bar/URLBar.swift

305 lines
11 KiB
Swift
Raw Normal View History

2020-07-24 19:26:35 -07:00
//
// URLBar.swift
// SBrowser
//
// Created by James Magahern on 7/23/20.
//
import UIKit
2021-03-09 00:14:48 -08:00
protocol URLBarDelegate: AnyObject
{
func urlBarRequestedFocusEscape(_ urlBar: URLBar)
}
class TextFieldWithKeyCommands: UITextField
{
internal var _keyCommands: [UIKeyCommand]? = []
override var keyCommands: [UIKeyCommand]? {
get { _keyCommands }
set { _keyCommands = newValue }
}
}
2020-09-22 14:33:00 -07:00
class URLBar: ReliefButton
2020-07-24 19:26:35 -07:00
{
2021-03-09 00:14:48 -08:00
let textField = TextFieldWithKeyCommands(frame: .zero)
2020-07-24 19:26:35 -07:00
let refreshButton = UIButton(frame: .zero)
2020-07-31 18:29:44 -07:00
let errorButton = UIButton(frame: .zero)
2020-09-22 15:37:13 -07:00
let documentButton = UIButton(frame: .zero)
2020-07-24 19:26:35 -07:00
2021-03-09 00:14:48 -08:00
weak var delegate: URLBarDelegate?
2020-07-28 11:31:30 -07:00
public enum LoadProgress {
case complete
case loading(progress: Double)
2020-07-31 18:29:44 -07:00
case error(error: Error)
2020-07-28 11:31:30 -07:00
}
public var loadProgress: LoadProgress = .complete {
didSet { updateProgressIndicator() }
}
2021-04-19 17:55:24 -07:00
override var isPointerInteractionEnabled: Bool {
get { false } set {}
}
2020-07-29 18:17:22 -07:00
private let fadeMaskView = UIImageView(frame: .zero)
2020-07-24 19:26:35 -07:00
private let progressIndicatorView = ProgressIndicatorView()
private var progressIndicatorAnimating = false
2021-06-14 16:32:51 -07:00
private let documentImage = UIImage(systemName: "ellipsis.circle")
2020-07-28 11:37:10 -07:00
private let refreshImage = UIImage(systemName: "arrow.clockwise")
private let stopImage = UIImage(systemName: "xmark")
2020-09-22 14:33:00 -07:00
private let backgroundCornerRadius: CGFloat = 0
2020-07-29 18:17:22 -07:00
2020-09-22 15:37:13 -07:00
private let documentSeparatorView = UIView(frame: .zero)
2020-09-22 14:33:00 -07:00
override init() {
super.init()
2020-07-24 19:26:35 -07:00
backgroundColor = .clear
constrainedToSquare = false
2020-07-24 19:26:35 -07:00
2020-09-22 14:33:00 -07:00
backgroundView.addSubview(progressIndicatorView)
2020-07-28 11:31:30 -07:00
2020-07-24 19:26:35 -07:00
textField.backgroundColor = .clear
textField.textContentType = .URL
textField.keyboardType = .webSearch
textField.autocorrectionType = .no
textField.autocapitalizationType = .none
2020-07-29 14:16:25 -07:00
textField.font = .systemFont(ofSize: 14.0)
textField.clearButtonMode = .whileEditing
textField.addAction(UIAction(handler: { [unowned self] _ in
// Mask view visibility is affected by editing state.
self.layoutSubviews()
}), for: [ .editingDidBegin, .editingDidEnd ])
2021-03-09 00:14:48 -08:00
textField.keyCommands = [
UIKeyCommand(action: #selector(Self.downKeyPressed), input: UIKeyCommand.inputDownArrow)
2021-06-10 21:39:32 -07:00
.prioritizeOverSystem()
2021-03-09 00:14:48 -08:00
]
2020-07-24 19:26:35 -07:00
addSubview(textField)
textField.addAction(.init(handler: { [textField, refreshButton] _ in
refreshButton.isHidden = textField.isFirstResponder
}), for: [ .editingDidBegin, .editingDidEnd ])
2020-07-24 19:26:35 -07:00
refreshButton.tintColor = .secondaryLabel
2020-07-28 11:37:10 -07:00
refreshButton.setImage(refreshImage, for: .normal)
2021-04-19 17:55:24 -07:00
refreshButton.isPointerInteractionEnabled = true
2020-07-24 19:26:35 -07:00
addSubview(refreshButton)
2020-07-31 18:29:44 -07:00
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)
2021-04-19 17:55:24 -07:00
errorButton.isPointerInteractionEnabled = true
2020-07-31 18:29:44 -07:00
addSubview(errorButton)
2020-09-22 15:37:13 -07:00
documentButton.tintColor = .secondaryLabel
documentButton.setImage(documentImage, for: .normal)
2021-04-19 17:55:24 -07:00
documentButton.isPointerInteractionEnabled = true
2020-09-22 15:37:13 -07:00
addSubview(documentButton)
documentSeparatorView.backgroundColor = .secondarySystemFill
addSubview(documentSeparatorView)
2020-07-31 18:29:44 -07:00
setErrorButtonAnimating(false)
2020-07-24 19:26:35 -07:00
}
2020-09-22 14:33:00 -07:00
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
2021-03-09 00:14:48 -08:00
@objc
private func downKeyPressed(_ sender: Any?) {
self.delegate?.urlBarRequestedFocusEscape(self)
}
2020-07-28 11:31:30 -07:00
private func updateProgressIndicator() {
2020-07-31 18:29:44 -07:00
setErrorButtonAnimating(false)
if progressIndicatorAnimating {
return
}
UIView.animate(withDuration: 0.4) { [unowned self] in
2020-07-28 11:31:30 -07:00
switch self.loadProgress {
case .complete:
2020-07-28 11:37:10 -07:00
self.refreshButton.setImage(self.refreshImage, for: .normal)
2020-07-28 11:31:30 -07:00
self.progressIndicatorView.progress = 1.0
self.progressIndicatorAnimating = true
2020-07-28 11:37:10 -07:00
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
self.progressIndicatorAnimating = false
2020-07-28 11:37:10 -07:00
}
2020-07-31 18:29:44 -07:00
2020-07-28 11:31:30 -07:00
case .loading(let progress):
2020-07-28 11:37:10 -07:00
self.refreshButton.setImage(self.stopImage, for: .normal)
2020-07-28 11:31:30 -07:00
self.progressIndicatorView.progress = progress
self.progressIndicatorView.alpha = 1.0
2020-07-31 18:29:44 -07:00
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)
}
2020-07-28 11:31:30 -07:00
}
}
2020-07-31 18:29:44 -07:00
self.setNeedsLayout()
2020-07-28 11:31:30 -07:00
}
2021-04-19 17:55:24 -07:00
override func setBackgroundInverted(_ inverted: Bool) {
2021-04-23 16:58:22 -05:00
// Do not invert.
super.setBackgroundInverted(false)
2021-04-19 17:55:24 -07:00
}
2020-07-28 11:31:30 -07:00
override var intrinsicContentSize: CGSize {
2020-07-24 19:26:35 -07:00
let preferredHeight = CGFloat(34)
return CGSize(width: 1000.0, height: preferredHeight)
}
2020-07-31 18:29:44 -07:00
private func fadeBackgroundImageForSize(_ size: CGSize, cutoffLocation: CGFloat) -> UIImage? {
2020-07-29 18:17:22 -07:00
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] = [
2020-07-31 18:29:44 -07:00
0.0, cutoffLocation, cutoffLocation + 0.10, 1.0
2020-07-29 18:17:22 -07:00
]
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
}
2020-07-31 18:29:44 -07:00
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)
}
}
2020-07-28 11:31:30 -07:00
override func layoutSubviews() {
2020-07-24 19:26:35 -07:00
super.layoutSubviews()
if let animationKeys = self.layer.animationKeys(), animationKeys.count > 0 {
// Kill the shadowPath while animating, since we cannot animate shadow paths apparently.
shadowView.layer.shadowPath = nil
}
2020-09-22 14:33:00 -07:00
progressIndicatorView.frame = backgroundView.bounds
2020-09-22 15:37:13 -07:00
// Document button
documentButton.frame = CGRect(x: 0.0, y: 0.0, width: bounds.height, height: bounds.height)
// Document separator
documentSeparatorView.frame = CGRect(
x: documentButton.frame.maxX, y: 0.0,
width: 1.0, height: bounds.height
)
documentSeparatorView.frame = documentSeparatorView.frame.insetBy(dx: 0.0, dy: 3.0)
// Text field
let textFieldPadding: CGFloat = 5.0
let textFieldOrigin = CGPoint(x: documentButton.frame.maxX + textFieldPadding, y: 0.0)
textField.frame = CGRect(
origin: textFieldOrigin,
size: CGSize(
width: bounds.width - textFieldOrigin.x - textFieldPadding,
height: bounds.height
)
)
2020-07-24 19:26:35 -07:00
2020-07-31 18:29:44 -07:00
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
2020-07-31 18:29:44 -07:00
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
2020-07-31 18:29:44 -07:00
}
// Fade mask
2020-07-29 18:17:22 -07:00
fadeMaskView.frame = textField.bounds
2020-07-31 18:29:44 -07:00
fadeMaskView.image = fadeBackgroundImageForSize(fadeMaskView.frame.size, cutoffLocation: fadeCutoffLocation)
2020-07-29 18:17:22 -07:00
if !textField.isFirstResponder {
textField.mask = fadeMaskView
} else {
textField.mask = nil
}
2020-07-24 19:26:35 -07:00
}
}
2020-07-28 11:31:30 -07:00
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))
}
}