Tabs implementation
Favicons and stuff too
This commit is contained in:
@@ -19,6 +19,8 @@ class BrowserView: UIView
|
||||
|
||||
var webView: WKWebView? {
|
||||
didSet {
|
||||
oldValue?.removeFromSuperview()
|
||||
|
||||
if let toolbarView = toolbarView {
|
||||
insertSubview(webView!, belowSubview: toolbarView)
|
||||
} else {
|
||||
@@ -36,12 +38,12 @@ class BrowserView: UIView
|
||||
|
||||
addSubview(titlebarView)
|
||||
|
||||
keyboardWillShowObserver = NotificationCenter.default.publisher(for: UIWindow.keyboardWillShowNotification).sink { notification in
|
||||
self.adjustOffsetForKeyboardNotification(userInfo: notification.userInfo!)
|
||||
keyboardWillShowObserver = NotificationCenter.default.publisher(for: UIWindow.keyboardWillShowNotification).sink { [weak self] notification in
|
||||
self?.adjustOffsetForKeyboardNotification(userInfo: notification.userInfo!)
|
||||
}
|
||||
|
||||
keyboardWillHideObserver = NotificationCenter.default.publisher(for: UIWindow.keyboardWillHideNotification).sink { notification in
|
||||
self.adjustOffsetForKeyboardNotification(userInfo: notification.userInfo!)
|
||||
keyboardWillHideObserver = NotificationCenter.default.publisher(for: UIWindow.keyboardWillHideNotification).sink { [weak self] notification in
|
||||
self?.adjustOffsetForKeyboardNotification(userInfo: notification.userInfo!)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +61,7 @@ class BrowserView: UIView
|
||||
}
|
||||
}(animationCurve)
|
||||
|
||||
self.keyboardLayoutOffset = bounds.height - keyboardEndFrame.minY
|
||||
keyboardLayoutOffset = bounds.height - keyboardEndFrame.minY
|
||||
UIView.animate(withDuration: animationDuration, delay: 0.0, options: animationOptions, animations: { self.layoutIfNeeded() }, completion: nil)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,20 +7,19 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
class BrowserViewController: UIViewController,
|
||||
SBRProcessBundleBridgeDelegate, WKNavigationDelegate,
|
||||
class BrowserViewController: UIViewController, WKNavigationDelegate,
|
||||
UITextFieldDelegate, ScriptPolicyViewControllerDelegate,
|
||||
UIPopoverPresentationControllerDelegate
|
||||
UIPopoverPresentationControllerDelegate, TabDelegate, TabPickerViewControllerDelegate
|
||||
{
|
||||
let bridge = SBRProcessBundleBridge()
|
||||
let browserView = BrowserView()
|
||||
var tab: Tab { didSet { didChangeTab(tab) } }
|
||||
var webView: WKWebView { tab.webView }
|
||||
|
||||
var javaScriptEnabledForTab: Bool = false
|
||||
|
||||
private let policyManager = ResourcePolicyManager()
|
||||
private let tabController = TabController()
|
||||
private let toolbarController = ToolbarViewController()
|
||||
private var allowedScriptOrigins = Set<String>()
|
||||
private var blockedScriptOrigins = Set<String>()
|
||||
|
||||
private var policyManager: ResourcePolicyManager { tabController.policyManager }
|
||||
|
||||
override var canBecomeFirstResponder: Bool { true }
|
||||
|
||||
private var titleObservation: NSKeyValueObservation?
|
||||
@@ -29,41 +28,37 @@ class BrowserViewController: UIViewController,
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle { .lightContent }
|
||||
|
||||
init() {
|
||||
self.tab = tabController.tabs.first!
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
addChild(toolbarController)
|
||||
didChangeTab(tab)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
override func loadView() {
|
||||
bridge.delegate = self
|
||||
bridge.policyDataSource = policyManager
|
||||
|
||||
addChild(toolbarController)
|
||||
|
||||
let webView = bridge.webView
|
||||
webView.allowsBackForwardNavigationGestures = true
|
||||
webView.navigationDelegate = self
|
||||
|
||||
browserView.webView = webView
|
||||
browserView.toolbarView = toolbarController.toolbarView
|
||||
|
||||
// Refresh button
|
||||
toolbarController.urlBar.refreshButton.addAction(UIAction(handler: { action in
|
||||
if webView.isLoading {
|
||||
webView.stopLoading()
|
||||
toolbarController.urlBar.refreshButton.addAction(UIAction(handler: { [weak self] action in
|
||||
guard let self = self else { return }
|
||||
if self.webView.isLoading {
|
||||
self.webView.stopLoading()
|
||||
} else {
|
||||
webView.reload()
|
||||
self.webView.reload()
|
||||
}
|
||||
}), for: .touchUpInside)
|
||||
|
||||
// Script button
|
||||
toolbarController.scriptControllerIconView.addAction(UIAction(handler: { action in
|
||||
let hostOrigin = webView.url?.host ?? ""
|
||||
let loadedScripts = self.allowedScriptOrigins.union(self.blockedScriptOrigins)
|
||||
toolbarController.scriptControllerIconView.addAction(UIAction(handler: { [weak self] action in
|
||||
guard let self = self else { return }
|
||||
let hostOrigin = self.webView.url?.host ?? ""
|
||||
let loadedScripts = self.tab.allowedScriptOrigins.union(self.tab.blockedScriptOrigins)
|
||||
let scriptViewController = ScriptPolicyViewController(policyManager: self.policyManager,
|
||||
hostOrigin: hostOrigin,
|
||||
loadedScripts: loadedScripts,
|
||||
scriptsAllowedForTab: self.javaScriptEnabledForTab)
|
||||
scriptsAllowedForTab: self.tab.javaScriptEnabled)
|
||||
scriptViewController.delegate = self
|
||||
|
||||
let navController = UINavigationController(rootViewController: scriptViewController)
|
||||
@@ -75,33 +70,76 @@ class BrowserViewController: UIViewController,
|
||||
}), for: .touchUpInside)
|
||||
|
||||
// Dark mode button
|
||||
toolbarController.darkModeButton.addAction(UIAction(handler: { _ in
|
||||
self.bridge.darkModeEnabled = !self.bridge.darkModeEnabled
|
||||
self.toolbarController.darkModeEnabled = self.bridge.darkModeEnabled
|
||||
toolbarController.darkModeButton.addAction(UIAction(handler: { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
self.tab.bridge.darkModeEnabled = !self.tab.bridge.darkModeEnabled
|
||||
self.toolbarController.darkModeEnabled = self.tab.bridge.darkModeEnabled
|
||||
}), for: .touchUpInside)
|
||||
|
||||
// Tabs button
|
||||
toolbarController.windowButton.addAction(UIAction(handler: { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
let tabPickerController = TabPickerViewController(tabController: self.tabController)
|
||||
tabPickerController.delegate = self
|
||||
tabPickerController.selectedTab = self.tab
|
||||
|
||||
let navController = UINavigationController(rootViewController: tabPickerController)
|
||||
navController.modalPresentationStyle = .popover
|
||||
navController.popoverPresentationController?.sourceView = self.toolbarController.windowButton
|
||||
navController.popoverPresentationController?.delegate = self
|
||||
|
||||
self.present(navController, animated: true, completion: nil)
|
||||
}), for: .touchUpInside)
|
||||
|
||||
// TextField delegate
|
||||
toolbarController.urlBar.textField.delegate = self
|
||||
|
||||
// Load progress
|
||||
loadingObservation = webView.observe(\.estimatedProgress) { (webView, observedChange) in
|
||||
if webView.estimatedProgress == 1.0 {
|
||||
self.toolbarController.urlBar.loadProgress = .complete
|
||||
} else {
|
||||
self.toolbarController.urlBar.loadProgress = .loading(progress: webView.estimatedProgress)
|
||||
}
|
||||
}
|
||||
|
||||
// Title observer
|
||||
titleObservation = webView.observe(\.title, changeHandler: { (webView, observedChange) in
|
||||
self.browserView.titlebarView.titleLabelView.text = webView.title
|
||||
})
|
||||
|
||||
self.view = browserView
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
beginLoadingURL(URL(string: "https://news.ycombinator.com")!)
|
||||
private func updateLoadProgress(forWebView webView: WKWebView) {
|
||||
if webView.estimatedProgress == 1.0 {
|
||||
toolbarController.urlBar.loadProgress = .complete
|
||||
} else {
|
||||
toolbarController.urlBar.loadProgress = .loading(progress: webView.estimatedProgress)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateTitleAndURL(forWebView webView: WKWebView) {
|
||||
browserView.titlebarView.titleLabelView.text = webView.title
|
||||
|
||||
if let urlString = webView.url?.absoluteString {
|
||||
toolbarController.urlBar.textField.text = urlString
|
||||
}
|
||||
}
|
||||
|
||||
private func didChangeTab(_ tab: Tab) {
|
||||
tab.delegate = self
|
||||
|
||||
let webView = tab.webView
|
||||
webView.allowsBackForwardNavigationGestures = true
|
||||
webView.navigationDelegate = self
|
||||
|
||||
// Change webView
|
||||
browserView.webView = webView
|
||||
|
||||
// Load progress
|
||||
updateLoadProgress(forWebView: webView)
|
||||
loadingObservation = webView.observe(\.estimatedProgress) { [weak self] (webView, observedChange) in
|
||||
self?.updateLoadProgress(forWebView: webView)
|
||||
}
|
||||
|
||||
// Title observer
|
||||
updateTitleAndURL(forWebView: webView)
|
||||
titleObservation = webView.observe(\.title, changeHandler: { [weak self] (webView, observedChange) in
|
||||
self?.updateTitleAndURL(forWebView: webView)
|
||||
})
|
||||
|
||||
// Script blocker button
|
||||
updateScriptBlockerButton()
|
||||
|
||||
// Dark mode status
|
||||
toolbarController.darkModeEnabled = tab.bridge.darkModeEnabled
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
@@ -110,14 +148,9 @@ class BrowserViewController: UIViewController,
|
||||
}
|
||||
|
||||
private func updateScriptBlockerButton() {
|
||||
toolbarController.scriptControllerIconView.setBlockedScriptsNumber(blockedScriptOrigins.count)
|
||||
toolbarController.scriptControllerIconView.setBlockedScriptsNumber(tab.blockedScriptOrigins.count)
|
||||
}
|
||||
|
||||
func beginLoadingURL(_ url: URL) {
|
||||
let request = URLRequest(url: url)
|
||||
bridge.webView.load(request)
|
||||
}
|
||||
|
||||
|
||||
// MARK: UIPopoverPresentationControllerDelegate
|
||||
|
||||
func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle {
|
||||
@@ -125,28 +158,17 @@ class BrowserViewController: UIViewController,
|
||||
return .none
|
||||
}
|
||||
|
||||
// MARK: SBRProcessBundleBridgeDelegate
|
||||
|
||||
func webProcess(_ bridge: SBRProcessBundleBridge, didAllowScriptResourceFromOrigin origin: String) {
|
||||
print("Allowed script resource from origin: \(origin)")
|
||||
allowedScriptOrigins.formUnion([ origin ])
|
||||
updateScriptBlockerButton()
|
||||
}
|
||||
|
||||
func webProcess(_ bridge: SBRProcessBundleBridge, didBlockScriptResourceFromOrigin origin: String) {
|
||||
print("Blocked script resource from origin: \(origin)")
|
||||
blockedScriptOrigins.formUnion([ origin ])
|
||||
updateScriptBlockerButton()
|
||||
}
|
||||
|
||||
// MARK: Navigation Delegate
|
||||
|
||||
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
|
||||
// Reset tracking this
|
||||
blockedScriptOrigins.removeAll()
|
||||
tab.blockedScriptOrigins.removeAll()
|
||||
|
||||
if let urlString = webView.url?.absoluteString {
|
||||
toolbarController.urlBar.textField.text = urlString
|
||||
updateTitleAndURL(forWebView: webView)
|
||||
|
||||
// Start requesting favicon
|
||||
if let url = webView.url {
|
||||
tab.updateFaviconForURL(url)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,7 +178,7 @@ class BrowserViewController: UIViewController,
|
||||
|
||||
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, preferences: WKWebpagePreferences, decisionHandler: @escaping (WKNavigationActionPolicy, WKWebpagePreferences) -> Void)
|
||||
{
|
||||
var allowJavaScript = javaScriptEnabledForTab
|
||||
var allowJavaScript = tab.javaScriptEnabled
|
||||
if !allowJavaScript, let host = navigationAction.request.url?.host {
|
||||
// Check origin policy
|
||||
allowJavaScript = policyManager.allowedOriginsForScriptResources().contains(host)
|
||||
@@ -173,7 +195,7 @@ class BrowserViewController: UIViewController,
|
||||
if url.scheme == nil {
|
||||
let urlString = "https://\(text)"
|
||||
if let url = URL(string: urlString) {
|
||||
beginLoadingURL(url)
|
||||
tab.beginLoadingURL(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -182,16 +204,38 @@ class BrowserViewController: UIViewController,
|
||||
return false
|
||||
}
|
||||
|
||||
// MARK: Tab Delegate
|
||||
|
||||
func didBlockScriptOrigin(_ origin: String, forTab: Tab) {
|
||||
updateScriptBlockerButton()
|
||||
}
|
||||
|
||||
// MARK: Tab Picker Delegate
|
||||
|
||||
func tabPicker(_ picker: TabPickerViewController, didSelectTab tab: Tab) {
|
||||
self.tab = tab
|
||||
picker.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
func tabPicker(_ picker: TabPickerViewController, willCloseTab tab: Tab) {
|
||||
// If closed tab is current tab, pick another one.
|
||||
if tab == self.tab {
|
||||
if let nextTab = tabController.tabs.last(where: { $0 != tab }) {
|
||||
self.tab = nextTab
|
||||
}
|
||||
|
||||
picker.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Script Policy View Controller Delegate
|
||||
|
||||
func didChangeScriptPolicy() {
|
||||
bridge.policyDataSourceDidChange()
|
||||
bridge.webView.reload()
|
||||
tab.bridge.policyDataSourceDidChange()
|
||||
webView.reload()
|
||||
}
|
||||
|
||||
func setScriptsEnabledForTab(_ enabled: Bool) {
|
||||
javaScriptEnabledForTab = enabled
|
||||
bridge.allowAllScripts = enabled
|
||||
tab.javaScriptEnabled = enabled
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
//
|
||||
// TitlebarView.swift
|
||||
// SBrowser
|
||||
//
|
||||
// Created by James Magahern on 7/29/20.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class TitlebarView: UIView
|
||||
{
|
||||
public let titleLabelView = UILabel(frame: .zero)
|
||||
private let backgroundImageView = UIImageView(frame: .zero)
|
||||
|
||||
convenience init() {
|
||||
self.init(frame: .zero)
|
||||
addSubview(backgroundImageView)
|
||||
addSubview(titleLabelView)
|
||||
|
||||
titleLabelView.textColor = .white
|
||||
titleLabelView.layer.shadowColor = UIColor.black.cgColor
|
||||
titleLabelView.layer.shadowRadius = 0.0
|
||||
titleLabelView.layer.shadowOffset = CGSize(width: 0.0, height: 2.0)
|
||||
titleLabelView.font = UIFont.boldSystemFont(ofSize: 12.0)
|
||||
}
|
||||
|
||||
private func backgroundImageForSize(_ size: CGSize) -> UIImage? {
|
||||
var image: UIImage? = nil
|
||||
|
||||
UIGraphicsBeginImageContext(CGSize(width: size.width, height: 1.0))
|
||||
if let context = UIGraphicsGetCurrentContext() {
|
||||
let gradientColorsArray = [
|
||||
UIColor(red: 0.101, green: 0.176, blue: 0.415, alpha: 1.0).cgColor,
|
||||
UIColor(red: 0.153, green: 0.000, blue: 0.153, alpha: 1.0).cgColor
|
||||
]
|
||||
|
||||
if let gradient = CGGradient(colorsSpace: nil, colors: gradientColorsArray as CFArray, locations: nil) {
|
||||
context.drawLinearGradient(gradient, start: .zero, end: CGPoint(x: size.width, y: 0.0), options: CGGradientDrawingOptions())
|
||||
}
|
||||
|
||||
image = UIGraphicsGetImageFromCurrentImageContext()
|
||||
UIGraphicsEndImageContext();
|
||||
}
|
||||
|
||||
return image
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
backgroundImageView.frame = bounds
|
||||
titleLabelView.frame = bounds.avoiding(verticalInsets: safeAreaInsets).insetBy(dx: 8.0 + layoutMargins.left, dy: 0.0)
|
||||
|
||||
if let image = backgroundImageView.image, image.size == bounds.size {
|
||||
// No op
|
||||
} else {
|
||||
backgroundImageView.image = backgroundImageForSize(bounds.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,198 +0,0 @@
|
||||
//
|
||||
// ToolbarViewController.swift
|
||||
// SBrowser
|
||||
//
|
||||
// Created by James Magahern on 7/23/20.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class ToolbarButtonView: UIView
|
||||
{
|
||||
private var buttonPadding = CGFloat(24.0)
|
||||
private var buttonViews: [UIView] = []
|
||||
|
||||
func addButtonView(_ button: UIView) {
|
||||
buttonViews.append(button)
|
||||
addSubview(button)
|
||||
setNeedsLayout()
|
||||
}
|
||||
|
||||
func removeAllButtonViews() {
|
||||
buttonViews.forEach { $0.removeFromSuperview() }
|
||||
buttonViews.removeAll()
|
||||
setNeedsLayout()
|
||||
}
|
||||
|
||||
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
let width: CGFloat = buttonViews.reduce(0.0) { (result, button) -> CGFloat in
|
||||
return result + button.sizeThatFits(size).width + buttonPadding
|
||||
}
|
||||
|
||||
return CGSize(width: width, height: size.height)
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
var buttonRect = CGRect(origin: .zero, size: CGSize(width: 0, height: bounds.height))
|
||||
buttonRect.origin.x = layoutMargins.left
|
||||
|
||||
for button in buttonViews {
|
||||
let buttonSize = button.sizeThatFits(bounds.size)
|
||||
buttonRect.size = CGSize(width: buttonSize.width, height: bounds.height)
|
||||
button.frame = buttonRect
|
||||
|
||||
buttonRect.origin.x += buttonRect.width + buttonPadding
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ToolbarView: UIView
|
||||
{
|
||||
var urlBar: URLBar? { didSet { containerView.addSubview(urlBar!) } }
|
||||
|
||||
var cancelButtonVisible: Bool = false { didSet { layoutSubviews() } }
|
||||
|
||||
let containerView = UIView(frame: .zero)
|
||||
let backgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .systemThickMaterial))
|
||||
let buttonsView = ToolbarButtonView(frame: .zero)
|
||||
let cancelButton = UIButton(type: .system)
|
||||
|
||||
convenience init()
|
||||
{
|
||||
self.init(frame: .zero)
|
||||
addSubview(backgroundView)
|
||||
addSubview(containerView)
|
||||
|
||||
containerView.addSubview(buttonsView)
|
||||
|
||||
cancelButton.setTitle("Cancel", for: .normal)
|
||||
containerView.addSubview(cancelButton)
|
||||
|
||||
layer.masksToBounds = false
|
||||
layer.shadowColor = UIColor.black.cgColor
|
||||
layer.shadowOpacity = 0.2
|
||||
layer.shadowOffset = CGSize(width: 0.0, height: 1.0)
|
||||
layer.shadowRadius = 1.5
|
||||
}
|
||||
|
||||
override func sizeThatFits(_ size: CGSize) -> CGSize
|
||||
{
|
||||
return CGSize(width: size.width, height: 44.0)
|
||||
}
|
||||
|
||||
override func layoutSubviews()
|
||||
{
|
||||
super.layoutSubviews()
|
||||
|
||||
backgroundView.frame = bounds
|
||||
|
||||
var containerBounds = bounds
|
||||
containerBounds.size.height -= safeAreaInsets.bottom
|
||||
containerView.frame = containerBounds
|
||||
containerView.frame = containerView.frame.insetBy(dx: 8.0, dy: 4.0)
|
||||
|
||||
// Cancel button
|
||||
let urlBarPadding: CGFloat = 8.0
|
||||
let cancelButtonSize = cancelButton.sizeThatFits(containerView.bounds.size)
|
||||
cancelButton.frame = CGRect(origin: CGPoint(x: (containerView.bounds.maxX - cancelButtonSize.width), y: 0),
|
||||
size: CGSize(width: cancelButtonSize.width, height: containerView.bounds.height))
|
||||
|
||||
// Toolbar buttons
|
||||
let buttonContainerSize = buttonsView.sizeThatFits(containerView.bounds.size)
|
||||
buttonsView.frame = CGRect(origin: CGPoint(x: (containerView.bounds.maxX - buttonContainerSize.width) + urlBarPadding, y: 0), size: buttonContainerSize)
|
||||
|
||||
var avoidingSize: CGSize = .zero
|
||||
if cancelButtonVisible {
|
||||
cancelButton.alpha = 1.0
|
||||
buttonsView.alpha = 0.0
|
||||
|
||||
avoidingSize = cancelButtonSize
|
||||
} else {
|
||||
cancelButton.alpha = 0.0
|
||||
buttonsView.alpha = 1.0
|
||||
|
||||
avoidingSize = buttonContainerSize
|
||||
}
|
||||
|
||||
if let urlBar = urlBar {
|
||||
urlBar.frame = CGRect(origin: .zero, size: CGSize(width: containerView.bounds.width - avoidingSize.width - urlBarPadding, height: containerView.bounds.height))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ToolbarViewController: UIViewController
|
||||
{
|
||||
let urlBar = URLBar()
|
||||
let toolbarView = ToolbarView()
|
||||
let scriptControllerIconView = ScriptControllerIconView()
|
||||
let shareButton = UIButton(frame: .zero)
|
||||
let darkModeButton = UIButton(frame: .zero)
|
||||
let windowButton = UIButton(frame: .zero)
|
||||
|
||||
var darkModeEnabled: Bool = false {
|
||||
didSet {
|
||||
if darkModeEnabled {
|
||||
darkModeButton.setImage(darkModeEnabledImage, for: .normal)
|
||||
} else {
|
||||
darkModeButton.setImage(darkModeDisabledImage, for: .normal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let darkModeDisabledImage = UIImage(systemName: "moon.circle")
|
||||
private let darkModeEnabledImage = UIImage(systemName: "moon.circle.fill")
|
||||
|
||||
init() {
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
toolbarView.urlBar = urlBar
|
||||
|
||||
// Dark mode button
|
||||
darkModeButton.setImage(darkModeDisabledImage, for: .normal)
|
||||
|
||||
// Share button
|
||||
shareButton.setImage(UIImage(systemName: "square.and.arrow.up"), for: .normal)
|
||||
|
||||
// Window button
|
||||
windowButton.setImage(UIImage(systemName: "rectangle.on.rectangle"), for: .normal)
|
||||
|
||||
let toolbarAnimationDuration: TimeInterval = 0.3
|
||||
urlBar.textField.addAction(.init(handler: { _ in
|
||||
UIView.animate(withDuration: toolbarAnimationDuration) {
|
||||
self.toolbarView.cancelButtonVisible = self.urlBar.textField.isFirstResponder
|
||||
}
|
||||
}), for: [ .editingDidBegin, .editingDidEnd ])
|
||||
|
||||
toolbarView.cancelButton.addAction(.init(handler: { action in
|
||||
self.urlBar.textField.resignFirstResponder()
|
||||
}), for: .touchUpInside)
|
||||
|
||||
traitCollectionDidChange(nil)
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
|
||||
toolbarView.buttonsView.removeAllButtonViews()
|
||||
|
||||
// Setup toolbar based on trait collection
|
||||
if traitCollection.horizontalSizeClass == .compact {
|
||||
toolbarView.buttonsView.addButtonView(darkModeButton)
|
||||
toolbarView.buttonsView.addButtonView(scriptControllerIconView)
|
||||
toolbarView.buttonsView.addButtonView(windowButton)
|
||||
} else {
|
||||
toolbarView.buttonsView.addButtonView(darkModeButton)
|
||||
toolbarView.buttonsView.addButtonView(shareButton)
|
||||
toolbarView.buttonsView.addButtonView(scriptControllerIconView)
|
||||
toolbarView.buttonsView.addButtonView(windowButton)
|
||||
}
|
||||
}
|
||||
|
||||
override func loadView() {
|
||||
self.view = toolbarView
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
//
|
||||
// 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)
|
||||
|
||||
public enum LoadProgress {
|
||||
case complete
|
||||
case loading(progress: Double)
|
||||
}
|
||||
|
||||
public var loadProgress: LoadProgress = .complete {
|
||||
didSet { updateProgressIndicator() }
|
||||
}
|
||||
|
||||
private let backgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .systemThickMaterial))
|
||||
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.systemFill.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
|
||||
addSubview(textField)
|
||||
|
||||
textField.addAction(.init(handler: { _ in
|
||||
self.refreshButton.isHidden = self.textField.isFirstResponder
|
||||
}), for: [ .editingDidBegin, .editingDidEnd ])
|
||||
|
||||
refreshButton.tintColor = .secondaryLabel
|
||||
refreshButton.setImage(refreshImage, for: .normal)
|
||||
addSubview(refreshButton)
|
||||
}
|
||||
|
||||
private func updateProgressIndicator() {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override var intrinsicContentSize: CGSize {
|
||||
let preferredHeight = CGFloat(34)
|
||||
return CGSize(width: 1000.0, height: preferredHeight)
|
||||
}
|
||||
|
||||
private func fadeBackgroundImageForSize(_ size: CGSize) -> 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, 0.80, 0.90, 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
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
backgroundView.frame = bounds
|
||||
progressIndicatorView.frame = backgroundView.contentView.bounds
|
||||
textField.frame = bounds.insetBy(dx: 6.0, dy: 0)
|
||||
|
||||
fadeMaskView.frame = textField.bounds
|
||||
fadeMaskView.image = fadeBackgroundImageForSize(fadeMaskView.frame.size)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user