From 6baf543da3ab9f9f4331ece04ccddcd4459ccbb6 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Thu, 30 Jul 2020 23:54:20 -0700 Subject: [PATCH] Tabs implementation Favicons and stuff too --- SBrowser.xcodeproj/project.pbxproj | 34 ++- .../xcshareddata/xcschemes/SBrowser.xcscheme | 7 + SBrowser/Browser View/BrowserView.swift | 12 +- .../Browser View/BrowserViewController.swift | 198 +++++++++++------- .../ScriptPolicyControl.swift | 12 +- .../ScriptPolicyViewController.swift | 13 +- SBrowser/Tabs/Tab.swift | 104 +++++++++ SBrowser/Tabs/TabController.swift | 40 ++++ SBrowser/Tabs/TabPickerViewController.swift | 125 +++++++++++ .../TitlebarView.swift | 0 .../ToolbarViewController.swift | 7 +- .../URLBar.swift | 6 +- .../SBRProcessBundleBridge.h | 1 + .../SBRProcessBundleBridge.m | 33 ++- 14 files changed, 487 insertions(+), 105 deletions(-) create mode 100644 SBrowser/Tabs/Tab.swift create mode 100644 SBrowser/Tabs/TabController.swift create mode 100644 SBrowser/Tabs/TabPickerViewController.swift rename SBrowser/{Browser View => Titlebar and URL Bar}/TitlebarView.swift (100%) rename SBrowser/{Browser View => Titlebar and URL Bar}/ToolbarViewController.swift (96%) rename SBrowser/{Browser View => Titlebar and URL Bar}/URLBar.swift (96%) diff --git a/SBrowser.xcodeproj/project.pbxproj b/SBrowser.xcodeproj/project.pbxproj index b7840b3..392f9d4 100644 --- a/SBrowser.xcodeproj/project.pbxproj +++ b/SBrowser.xcodeproj/project.pbxproj @@ -9,6 +9,9 @@ /* Begin PBXBuildFile section */ 1A14FC2324D203D9009B3F83 /* TitlebarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A14FC2224D203D9009B3F83 /* TitlebarView.swift */; }; 1A14FC2624D251BD009B3F83 /* darkmode.css in Resources */ = {isa = PBXBuildFile; fileRef = 1A14FC2524D251BD009B3F83 /* darkmode.css */; }; + 1A14FC2824D26749009B3F83 /* Tab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A14FC2724D26749009B3F83 /* Tab.swift */; }; + 1AB88EFD24D3BA560006F850 /* TabController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AB88EFC24D3BA560006F850 /* TabController.swift */; }; + 1AB88EFF24D3BBA50006F850 /* TabPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AB88EFE24D3BBA50006F850 /* TabPickerViewController.swift */; }; 1ADFF46024C7DE53006DC7AE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ADFF45F24C7DE53006DC7AE /* AppDelegate.swift */; }; 1ADFF46224C7DE53006DC7AE /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ADFF46124C7DE53006DC7AE /* SceneDelegate.swift */; }; 1ADFF46924C7DE54006DC7AE /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1ADFF46824C7DE54006DC7AE /* Assets.xcassets */; }; @@ -56,6 +59,9 @@ /* Begin PBXFileReference section */ 1A14FC2224D203D9009B3F83 /* TitlebarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitlebarView.swift; sourceTree = ""; }; 1A14FC2524D251BD009B3F83 /* darkmode.css */ = {isa = PBXFileReference; lastKnownFileType = text.css; path = darkmode.css; sourceTree = ""; }; + 1A14FC2724D26749009B3F83 /* Tab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tab.swift; sourceTree = ""; }; + 1AB88EFC24D3BA560006F850 /* TabController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabController.swift; sourceTree = ""; }; + 1AB88EFE24D3BBA50006F850 /* TabPickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabPickerViewController.swift; sourceTree = ""; }; 1ADFF45C24C7DE53006DC7AE /* SBrowser.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SBrowser.app; sourceTree = BUILT_PRODUCTS_DIR; }; 1ADFF45F24C7DE53006DC7AE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 1ADFF46124C7DE53006DC7AE /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; @@ -112,6 +118,26 @@ path = Resources; sourceTree = ""; }; + 1AB88F0324D3E1EC0006F850 /* Tabs */ = { + isa = PBXGroup; + children = ( + 1A14FC2724D26749009B3F83 /* Tab.swift */, + 1AB88EFC24D3BA560006F850 /* TabController.swift */, + 1AB88EFE24D3BBA50006F850 /* TabPickerViewController.swift */, + ); + path = Tabs; + sourceTree = ""; + }; + 1AB88F0424D3E1F90006F850 /* Titlebar and URL Bar */ = { + isa = PBXGroup; + children = ( + 1A14FC2224D203D9009B3F83 /* TitlebarView.swift */, + 1ADFF4C824CA793E006DC7AE /* ToolbarViewController.swift */, + 1ADFF4BF24CA6964006DC7AE /* URLBar.swift */, + ); + path = "Titlebar and URL Bar"; + sourceTree = ""; + }; 1ADFF45324C7DE53006DC7AE = { isa = PBXGroup; children = ( @@ -139,6 +165,8 @@ 1ADFF47A24C7E176006DC7AE /* Backend */, 1ADFF47724C7DFE8006DC7AE /* Browser View */, 1ADFF4CE24CBBCBD006DC7AE /* Script Policy UI */, + 1AB88F0324D3E1EC0006F850 /* Tabs */, + 1AB88F0424D3E1F90006F850 /* Titlebar and URL Bar */, 1ADFF4C124CA6AE4006DC7AE /* Utilities */, 1ADFF4AF24C92E2F006DC7AE /* Web Process Bundle Bridge */, 1A14FC2424D2517A009B3F83 /* Resources */, @@ -164,9 +192,6 @@ children = ( 1ADFF47324C7DE9C006DC7AE /* BrowserViewController.swift */, 1ADFF47824C7DFF8006DC7AE /* BrowserView.swift */, - 1A14FC2224D203D9009B3F83 /* TitlebarView.swift */, - 1ADFF4C824CA793E006DC7AE /* ToolbarViewController.swift */, - 1ADFF4BF24CA6964006DC7AE /* URLBar.swift */, ); path = "Browser View"; sourceTree = ""; @@ -337,10 +362,13 @@ 1ADFF48D24C8C176006DC7AE /* SBRProcessBundleBridge.m in Sources */, 1ADFF46224C7DE53006DC7AE /* SceneDelegate.swift in Sources */, 1ADFF4CB24CB8278006DC7AE /* ScriptControllerIconView.swift in Sources */, + 1AB88EFD24D3BA560006F850 /* TabController.swift in Sources */, 1ADFF4C324CA6AF6006DC7AE /* CGPoint+Utils.swift in Sources */, 1ADFF4C924CA793E006DC7AE /* ToolbarViewController.swift in Sources */, 1ADFF4CD24CBB0C8006DC7AE /* ScriptPolicyViewController.swift in Sources */, + 1A14FC2824D26749009B3F83 /* Tab.swift in Sources */, 1ADFF47924C7DFF8006DC7AE /* BrowserView.swift in Sources */, + 1AB88EFF24D3BBA50006F850 /* TabPickerViewController.swift in Sources */, 1A14FC2324D203D9009B3F83 /* TitlebarView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/SBrowser.xcodeproj/xcshareddata/xcschemes/SBrowser.xcscheme b/SBrowser.xcodeproj/xcshareddata/xcschemes/SBrowser.xcscheme index 085e76f..d38f344 100644 --- a/SBrowser.xcodeproj/xcshareddata/xcschemes/SBrowser.xcscheme +++ b/SBrowser.xcodeproj/xcshareddata/xcschemes/SBrowser.xcscheme @@ -51,6 +51,13 @@ ReferencedContainer = "container:SBrowser.xcodeproj"> + + + + () - private var blockedScriptOrigins = Set() + + 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 } - } diff --git a/SBrowser/Script Policy UI/ScriptPolicyControl.swift b/SBrowser/Script Policy UI/ScriptPolicyControl.swift index 30f730d..fd09656 100644 --- a/SBrowser/Script Policy UI/ScriptPolicyControl.swift +++ b/SBrowser/Script Policy UI/ScriptPolicyControl.swift @@ -30,16 +30,16 @@ class ScriptPolicyControl: UIControl convenience init() { self.init(frame: .zero) - allowButton.addAction(UIAction(handler: { _ in - self.policyStatus = .allowed - self.sendActions(for: .valueChanged) + allowButton.addAction(UIAction(handler: { [weak self] _ in + self?.policyStatus = .allowed + self?.sendActions(for: .valueChanged) }), for: .touchUpInside) allowButton.imageView?.contentMode = .scaleAspectFit addSubview(allowButton) - denyButton.addAction(UIAction(handler: { _ in - self.policyStatus = .blocked - self.sendActions(for: .valueChanged) + denyButton.addAction(UIAction(handler: { [weak self] _ in + self?.policyStatus = .blocked + self?.sendActions(for: .valueChanged) }), for: .touchUpInside) denyButton.imageView?.contentMode = .scaleAspectFit addSubview(denyButton) diff --git a/SBrowser/Script Policy UI/ScriptPolicyViewController.swift b/SBrowser/Script Policy UI/ScriptPolicyViewController.swift index 739de72..4938b3b 100644 --- a/SBrowser/Script Policy UI/ScriptPolicyViewController.swift +++ b/SBrowser/Script Policy UI/ScriptPolicyViewController.swift @@ -7,7 +7,7 @@ import UIKit -protocol ScriptPolicyViewControllerDelegate { +protocol ScriptPolicyViewControllerDelegate: class { func didChangeScriptPolicy() func setScriptsEnabledForTab(_ enabled: Bool) } @@ -77,8 +77,8 @@ class SwitchListCell: UICollectionViewListCell class ScriptPolicyViewController: UIViewController, UICollectionViewDelegate { var collectionView: UICollectionView? - var delegate: ScriptPolicyViewControllerDelegate? = nil var allowScriptsForTab = false + weak var delegate: ScriptPolicyViewControllerDelegate? = nil private var dataSource: UICollectionViewDiffableDataSource? private var didChangeScriptPolicy = false @@ -102,7 +102,8 @@ class ScriptPolicyViewController: UIViewController, UICollectionViewDelegate let otherOriginScripts = loadedScripts.subtracting([ hostOrigin ]) let originItems = [ hostOrigin ] + otherOriginScripts - let switchCellRegistry = UICollectionView.CellRegistration { (listCell, indexPath, item) in + let switchCellRegistry = UICollectionView.CellRegistration { [weak self] (listCell, indexPath, item) in + guard let self = self else { return } var config = listCell.defaultContentConfiguration() if item == Self.enableScriptsForTabItem { config.text = "Allow for Tab" @@ -132,7 +133,8 @@ class ScriptPolicyViewController: UIViewController, UICollectionViewDelegate listCell.contentConfiguration = config } - let scriptPolicyRegistry = UICollectionView.CellRegistration { (listCell, indexPath, item) in + let scriptPolicyRegistry = UICollectionView.CellRegistration { [weak self] (listCell, indexPath, item) in + guard let self = self else { return } var config = listCell.defaultContentConfiguration() config.text = item @@ -194,7 +196,8 @@ class ScriptPolicyViewController: UIViewController, UICollectionViewDelegate self.collectionView = collectionView title = "Script Origin Policy" - navigationItem.rightBarButtonItem = UIBarButtonItem(systemItem: .done, primaryAction: UIAction(handler: { action in + navigationItem.rightBarButtonItem = UIBarButtonItem(systemItem: .done, primaryAction: UIAction(handler: { [weak self] action in + guard let self = self else { return } if self.didChangeScriptPolicy { self.delegate?.didChangeScriptPolicy() self.delegate?.setScriptsEnabledForTab(self.allowScriptsForTab) diff --git a/SBrowser/Tabs/Tab.swift b/SBrowser/Tabs/Tab.swift new file mode 100644 index 0000000..84690dd --- /dev/null +++ b/SBrowser/Tabs/Tab.swift @@ -0,0 +1,104 @@ +// +// Tab.swift +// SBrowser +// +// Created by James Magahern on 7/29/20. +// + +import UIKit +import Combine + +protocol TabDelegate: class +{ + func didBlockScriptOrigin(_ origin: String, forTab: Tab) +} + +class Tab: NSObject, SBRProcessBundleBridgeDelegate +{ + public weak var delegate: TabDelegate? + + public let homeURL: URL + public let bridge = SBRProcessBundleBridge() + public var webView: WKWebView { + if self.loadedWebView == nil { + self.loadedWebView = bridge.webView + beginLoadingURL(homeURL) + } + + return bridge.webView + } + public var policyManager: ResourcePolicyManager + + private var loadedWebView: WKWebView? = nil + public var title: String? { loadedWebView?.title } + public var url: URL? { loadedWebView?.url ?? self.homeURL } + + public var javaScriptEnabled: Bool = false { + didSet { bridge.allowAllScripts = javaScriptEnabled } + } + + public var identifier = UUID() + + public var favicon: UIImage? + private var faviconHost: String? + private var faviconRequest: AnyCancellable? + + public var allowedScriptOrigins = Set() + public var blockedScriptOrigins = Set() + + private var titleObservation: NSKeyValueObservation? + private var urlObservation: NSKeyValueObservation? + + convenience init(urlString: String, policyManager: ResourcePolicyManager) { + self.init(url: URL(string: urlString)!, policyManager: policyManager) + } + + init(url: URL, policyManager: ResourcePolicyManager) { + self.homeURL = url + self.policyManager = policyManager + bridge.policyDataSource = policyManager + + super.init() + + bridge.delegate = self + } + + deinit { + bridge.tearDown() + } + + func beginLoadingURL(_ url: URL) { + let request = URLRequest(url: url) + webView.load(request) + } + + // MARK: SBRProcessBundleBridgeDelegate + + func webProcess(_ bridge: SBRProcessBundleBridge, didAllowScriptResourceFromOrigin origin: String) { + print("Allowed script resource from origin: \(origin)") + allowedScriptOrigins.formUnion([ origin ]) + } + + func webProcess(_ bridge: SBRProcessBundleBridge, didBlockScriptResourceFromOrigin origin: String) { + print("Blocked script resource from origin: \(origin)") + blockedScriptOrigins.formUnion([ origin ]) + delegate?.didBlockScriptOrigin(origin, forTab: self) + } + + func updateFaviconForURL(_ url: URL) { + if let faviconHost = faviconHost, url.host == faviconHost {} else { + guard var faviconURLComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return } + faviconURLComponents.path = "/favicon.ico" + + let defaultImage = UIImage(systemName: "globe") + guard let faviconURL = faviconURLComponents.url else { return } + faviconRequest = URLSession.shared.dataTaskPublisher(for: faviconURL) + .map { (data: Data, response: URLResponse) -> UIImage? in + UIImage(data: data) + } + .replaceError(with: defaultImage) + .replaceNil(with: defaultImage) + .assign(to: \.favicon, on: self) + } + } +} diff --git a/SBrowser/Tabs/TabController.swift b/SBrowser/Tabs/TabController.swift new file mode 100644 index 0000000..9448627 --- /dev/null +++ b/SBrowser/Tabs/TabController.swift @@ -0,0 +1,40 @@ +// +// TabController.swift +// SBrowser +// +// Created by James Magahern on 7/30/20. +// + +import Foundation + +class TabController +{ + var tabs: [Tab] = [] + var policyManager = ResourcePolicyManager() + + init() { + // TODO: load tabs from disk. + _ = createNewTab() + } + + func tab(forURL url: URL) -> Tab? { + tabs.first { $0.url == url } + } + + func tab(forIdentifier identifier: UUID) -> Tab? { + tabs.first { $0.identifier == identifier } + } + + func createNewTab() -> Tab { + let tab = Tab(urlString: "about:blank", policyManager: policyManager) + tabs.append(tab) + + return tab + } + + func closeTab(_ tab: Tab) { + if let index = tabs.firstIndex(of: tab) { + tabs.remove(at: index) + } + } +} diff --git a/SBrowser/Tabs/TabPickerViewController.swift b/SBrowser/Tabs/TabPickerViewController.swift new file mode 100644 index 0000000..682e1e7 --- /dev/null +++ b/SBrowser/Tabs/TabPickerViewController.swift @@ -0,0 +1,125 @@ +// +// TabPickerViewController.swift +// SBrowser +// +// Created by James Magahern on 7/30/20. +// + +import UIKit + +protocol TabPickerViewControllerDelegate: class +{ + func tabPicker(_ picker: TabPickerViewController, didSelectTab tab: Tab) + func tabPicker(_ picker: TabPickerViewController, willCloseTab tab: Tab) +} + +class TabPickerViewController: UIViewController, UICollectionViewDelegate +{ + let tabController: TabController! + var selectedTab: Tab? + + weak var delegate: TabPickerViewControllerDelegate? + + typealias TabID = UUID + + private var collectionView: UICollectionView? + private var dataSource: UICollectionViewDiffableDataSource? + + init(tabController: TabController) { + self.tabController = tabController + super.init(nibName: nil, bundle: nil) + + self.title = "Tabs" + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + var listConfig = UICollectionLayoutListConfiguration(appearance: .grouped) + listConfig.trailingSwipeActionsConfigurationProvider = { [weak self] indexPath in + if self?.dataSource?.snapshot().numberOfItems ?? 0 <= 1 { + return nil + } + + return UISwipeActionsConfiguration(actions: [ UIContextualAction(style: .destructive, title: "Close", handler: { [weak self] (action, view, completionHandler) in + guard let self = self else { return } + if let item = self.dataSource?.itemIdentifier(for: indexPath), var snapshot = self.dataSource?.snapshot() { + if let tab = self.tabController.tab(forIdentifier: item) { + self.delegate?.tabPicker(self, willCloseTab: tab) + + self.tabController.closeTab(tab) + snapshot.deleteItems([ item ]) + self.dataSource?.apply(snapshot, animatingDifferences: true) + } + } + })]) + } + + let listLayout = UICollectionViewCompositionalLayout.list(using: listConfig) + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: listLayout) + + let registry = UICollectionView.CellRegistration { [weak self] (listCell, indexPath, item) in + guard let self = self else { return } + var config = listCell.defaultContentConfiguration() + + if let tab = self.tabController.tab(forIdentifier: item) { + if let title = tab.title { + config.text = title + config.secondaryText = tab.url?.absoluteString + } else { + config.text = tab.url?.absoluteString + config.secondaryText = tab.url?.absoluteString + } + + if let image = tab.favicon { + config.image = image + config.imageProperties.maximumSize = CGSize(width: 21.0, height: 21.0) + config.imageProperties.cornerRadius = 3.0 + } + + if tab == self.selectedTab { + listCell.accessories = [ .checkmark() ] + } else { + listCell.accessories = [] + } + } + + listCell.contentConfiguration = config + } + + let dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { (collectionView, indexPath, item) -> UICollectionViewCell? in + return collectionView.dequeueConfiguredReusableCell(using: registry, for: indexPath, item: item) + } + + collectionView.dataSource = dataSource + collectionView.delegate = self + + var snapshot = dataSource.snapshot() + snapshot.appendSections([ 0 ]) + tabController.tabs.forEach { tab in + snapshot.appendItems([ tab.identifier ]) + } + + dataSource.apply(snapshot) + + self.dataSource = dataSource + self.collectionView = collectionView + self.view = self.collectionView + + let newTabButton = UIBarButtonItem(systemItem: .add, primaryAction: UIAction(handler: { [weak self] _ in + guard let self = self else { return } + + let newTab = self.tabController.createNewTab() + self.delegate?.tabPicker(self, didSelectTab: newTab) + }), menu: nil) + + navigationItem.rightBarButtonItem = newTabButton + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + let tab = tabController.tabs[indexPath.row] + delegate?.tabPicker(self, didSelectTab: tab) + } +} diff --git a/SBrowser/Browser View/TitlebarView.swift b/SBrowser/Titlebar and URL Bar/TitlebarView.swift similarity index 100% rename from SBrowser/Browser View/TitlebarView.swift rename to SBrowser/Titlebar and URL Bar/TitlebarView.swift diff --git a/SBrowser/Browser View/ToolbarViewController.swift b/SBrowser/Titlebar and URL Bar/ToolbarViewController.swift similarity index 96% rename from SBrowser/Browser View/ToolbarViewController.swift rename to SBrowser/Titlebar and URL Bar/ToolbarViewController.swift index 32b3b3e..9f222fd 100644 --- a/SBrowser/Browser View/ToolbarViewController.swift +++ b/SBrowser/Titlebar and URL Bar/ToolbarViewController.swift @@ -157,14 +157,15 @@ class ToolbarViewController: UIViewController windowButton.setImage(UIImage(systemName: "rectangle.on.rectangle"), for: .normal) let toolbarAnimationDuration: TimeInterval = 0.3 - urlBar.textField.addAction(.init(handler: { _ in + urlBar.textField.addAction(.init(handler: { [weak self] _ in + guard let self = self else { return } 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() + toolbarView.cancelButton.addAction(.init(handler: { [weak self] action in + self?.urlBar.textField.resignFirstResponder() }), for: .touchUpInside) traitCollectionDidChange(nil) diff --git a/SBrowser/Browser View/URLBar.swift b/SBrowser/Titlebar and URL Bar/URLBar.swift similarity index 96% rename from SBrowser/Browser View/URLBar.swift rename to SBrowser/Titlebar and URL Bar/URLBar.swift index 25a13ba..09b42d2 100644 --- a/SBrowser/Browser View/URLBar.swift +++ b/SBrowser/Titlebar and URL Bar/URLBar.swift @@ -54,8 +54,10 @@ class URLBar: UIView textField.clearButtonMode = .whileEditing addSubview(textField) - textField.addAction(.init(handler: { _ in - self.refreshButton.isHidden = self.textField.isFirstResponder + textField.addAction(.init(handler: { [weak self] _ in + if let self = self { + self.refreshButton.isHidden = self.textField.isFirstResponder + } }), for: [ .editingDidBegin, .editingDidEnd ]) refreshButton.tintColor = .secondaryLabel diff --git a/SBrowser/Web Process Bundle Bridge/SBRProcessBundleBridge.h b/SBrowser/Web Process Bundle Bridge/SBRProcessBundleBridge.h index ba65b10..19c7ae2 100644 --- a/SBrowser/Web Process Bundle Bridge/SBRProcessBundleBridge.h +++ b/SBrowser/Web Process Bundle Bridge/SBRProcessBundleBridge.h @@ -33,6 +33,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, assign) BOOL darkModeEnabled; - (void)policyDataSourceDidChange; +- (void)tearDown; @end diff --git a/SBrowser/Web Process Bundle Bridge/SBRProcessBundleBridge.m b/SBrowser/Web Process Bundle Bridge/SBRProcessBundleBridge.m index 5ea6a6d..a8cdbed 100644 --- a/SBrowser/Web Process Bundle Bridge/SBRProcessBundleBridge.m +++ b/SBrowser/Web Process Bundle Bridge/SBRProcessBundleBridge.m @@ -33,6 +33,33 @@ _WKUserStyleSheet *_darkModeStyleSheet; } +- (void)tearDown +{ + [[_webView _remoteObjectRegistry] unregisterExportedObject:self interface:[self _webProcessDelegateInterface]]; +} + +- (_WKRemoteObjectInterface *)_webProcessDelegateInterface +{ + static dispatch_once_t onceToken; + static _WKRemoteObjectInterface *interface = nil; + dispatch_once(&onceToken, ^{ + interface = [_WKRemoteObjectInterface remoteObjectInterfaceWithProtocol:@protocol(SBRWebProcessDelegate)]; + }); + + return interface; +} + +- (_WKRemoteObjectInterface *)_webProcessProxyInterface +{ + static dispatch_once_t onceToken; + static _WKRemoteObjectInterface *interface = nil; + dispatch_once(&onceToken, ^{ + interface = [_WKRemoteObjectInterface remoteObjectInterfaceWithProtocol:@protocol(SBRWebProcessProxy)]; + }); + + return interface; +} + - (WKWebView *)webView { if (!_webView) { @@ -56,12 +83,10 @@ WKWebView *webView = [[WKWebView alloc] initWithFrame:CGRectZero configuration:configuration]; // Configure proxy interface (interface to remote web process) - _WKRemoteObjectInterface *proxyInterface = [_WKRemoteObjectInterface remoteObjectInterfaceWithProtocol:@protocol(SBRWebProcessProxy)]; - _webProcessProxy = [[webView _remoteObjectRegistry] remoteObjectProxyWithInterface:proxyInterface]; + _webProcessProxy = [[webView _remoteObjectRegistry] remoteObjectProxyWithInterface:[self _webProcessProxyInterface]]; // Configure delegate interface (registering us as the web process delegate for the remote process) - _WKRemoteObjectInterface *delegateInterface = [_WKRemoteObjectInterface remoteObjectInterfaceWithProtocol:@protocol(SBRWebProcessDelegate)]; - [[webView _remoteObjectRegistry] registerExportedObject:self interface:delegateInterface]; + [[webView _remoteObjectRegistry] registerExportedObject:self interface:[self _webProcessDelegateInterface]]; _webView = webView; _webViewConfiguration = configuration;