// // BrowserViewController.swift // SBrowser // // Created by James Magahern on 7/21/20. // import UIKit import UniformTypeIdentifiers class BrowserViewController: UIViewController, WKNavigationDelegate, UITextFieldDelegate, ScriptPolicyViewControllerDelegate, UIPopoverPresentationControllerDelegate, TabDelegate, TabPickerViewControllerDelegate { let browserView = BrowserView() var tab: Tab { didSet { didChangeTab(tab) } } var webView: WKWebView { tab.webView } private let tabController = TabController() private let toolbarController = ToolbarViewController() private var policyManager: ResourcePolicyManager { tabController.policyManager } override var canBecomeFirstResponder: Bool { true } private var titleObservation: NSKeyValueObservation? private var loadingObservation: NSKeyValueObservation? private var backButtonObservation: NSKeyValueObservation? private var forwardButtonObservation: NSKeyValueObservation? 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() { browserView.toolbarView = toolbarController.toolbarView // Refresh button toolbarController.urlBar.refreshButton.addAction(UIAction(handler: { [webView] action in if webView.isLoading { webView.stopLoading() } else { webView.reload() } }), for: .touchUpInside) // Back button toolbarController.backButton.addAction(UIAction(handler: { [webView] _ in webView.goBack() }), for: .touchUpInside) // Forward button toolbarController.forwardButton.addAction(UIAction(handler: { [webView] _ in webView.goForward() }), for: .touchUpInside) // Share button toolbarController.shareButton.addAction(UIAction(handler: { [unowned self, webView, toolbarController] _ in if let url = webView.url { let itemProvider = NSItemProvider(item: url as NSURL, typeIdentifier: UTType.url.identifier) let config = UIActivityItemsConfiguration(itemProviders: [ itemProvider ]) config.metadataProvider = { metadataKey in switch metadataKey { case .title: return webView.title case .messageBody: return webView.title default: return nil } } config.previewProvider = { index, intent, suggestedSize in NSItemProvider(item: self.tab.favicon, typeIdentifier: UTType.image.identifier) } let activityController = UIActivityViewController(activityItemsConfiguration: config) activityController.popoverPresentationController?.sourceView = toolbarController.shareButton self.present(activityController, animated: true, completion: nil) } }), for: .touchUpInside) // Script button toolbarController.scriptControllerIconView.addAction(UIAction(handler: { [unowned self] action in 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.tab.javaScriptEnabled) scriptViewController.delegate = self let navController = UINavigationController(rootViewController: scriptViewController) navController.modalPresentationStyle = .popover navController.popoverPresentationController?.sourceView = self.toolbarController.scriptControllerIconView navController.popoverPresentationController?.delegate = self self.present(navController, animated: true, completion: nil) }), for: .touchUpInside) // Dark mode button toolbarController.darkModeButton.addAction(UIAction(handler: { [tab, toolbarController] _ in tab.bridge.darkModeEnabled = !tab.bridge.darkModeEnabled toolbarController.darkModeEnabled = tab.bridge.darkModeEnabled }), for: .touchUpInside) // Tabs button toolbarController.windowButton.addAction(UIAction(handler: { [unowned self] _ in 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 self.view = browserView } 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) { [unowned self] (webView, observedChange) in self.updateLoadProgress(forWebView: webView) } // Title observer updateTitleAndURL(forWebView: webView) titleObservation = webView.observe(\.title, changeHandler: { [unowned self] (webView, observedChange) in self.updateTitleAndURL(forWebView: webView) }) // Back/forward observer toolbarController.backButton.isEnabled = webView.canGoBack backButtonObservation = webView.observe(\.canGoBack, changeHandler: { [toolbarController] (webView, observedChange) in toolbarController.backButton.isEnabled = webView.canGoBack }) toolbarController.forwardButton.isEnabled = webView.canGoForward forwardButtonObservation = webView.observe(\.canGoForward, changeHandler: { [toolbarController] (webView, observedChange) in toolbarController.forwardButton.isEnabled = webView.canGoForward }) // Script blocker button updateScriptBlockerButton() // Dark mode status toolbarController.darkModeEnabled = tab.bridge.darkModeEnabled } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) becomeFirstResponder() } private func updateScriptBlockerButton() { toolbarController.scriptControllerIconView.setBlockedScriptsNumber(tab.blockedScriptOrigins.count) } // MARK: UIPopoverPresentationControllerDelegate func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle { // Forces popovers to present on iPhone return .none } // MARK: Navigation Delegate func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { // Reset tracking this tab.blockedScriptOrigins.removeAll() updateTitleAndURL(forWebView: webView) // Start requesting favicon if let url = webView.url { tab.updateFaviconForURL(url) } } func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { toolbarController.urlBar.loadProgress = .complete } func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, preferences: WKWebpagePreferences, decisionHandler: @escaping (WKNavigationActionPolicy, WKWebpagePreferences) -> Void) { var allowJavaScript = tab.javaScriptEnabled if !allowJavaScript, let host = navigationAction.request.url?.host { // Check origin policy allowJavaScript = policyManager.allowedOriginsForScriptResources().contains(host) } preferences.allowsContentJavaScript = allowJavaScript decisionHandler(.allow, preferences) } // MARK: UITextField Delegate func textFieldShouldReturn(_ textField: UITextField) -> Bool { if let text = textField.text, let url = URL(string: text) { if url.scheme == nil { let urlString = "https://\(text)" if let url = URL(string: urlString) { tab.beginLoadingURL(url) } } } textField.resignFirstResponder() 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() { tab.bridge.policyDataSourceDidChange() webView.reload() } func setScriptsEnabledForTab(_ enabled: Bool) { tab.javaScriptEnabled = enabled } }