// // BrowserViewController.swift // SBrowser // // Created by James Magahern on 7/21/20. // import Combine import MessageUI import UIKit import UniformTypeIdentifiers class BrowserViewController: UIViewController { let browserView = BrowserView() var currentTab: Tab { didSet { didChangeTab(currentTab) } } var webView: WKWebView { currentTab.webView } internal let tabController = TabController() internal let tabBarViewController: TabBarViewController internal let toolbarController = ToolbarViewController() internal let findOnPageController = FindOnPageViewController() internal let autocompleteViewController = AutocompleteViewController() internal var policyManager: ResourcePolicyManager { tabController.policyManager } override var canBecomeFirstResponder: Bool { true } private var loadingObservation: NSKeyValueObservation? private var backButtonObservation: NSKeyValueObservation? private var forwardButtonObservation: NSKeyValueObservation? private var hasSecureContentObservation: NSKeyValueObservation? private var activeTabObservation: AnyCancellable? internal var shiftKeyHeld: Bool = false internal var commandKeyHeld: Bool = false internal var windowButtonHeld: Bool { get { toolbarController.newTabButton.isTracking } set { toolbarController.newTabButton.cancelTracking(with: nil) } } static let longPressWindowButtonToMakeNewTab: Bool = false internal var darkModeEnabled: Bool { get { currentTab.bridge.darkModeEnabled } set { currentTab.bridge.darkModeEnabled = newValue toolbarController.darkModeEnabled = newValue } } internal enum PreferredEmailSharingRecipient: String, CaseIterable { case readLater = "Read Later" case bookmark = "Bookmark" case other = "Other…" } override var preferredStatusBarStyle: UIStatusBarStyle { .lightContent } internal var changingFocusToAutocompleteController = false init() { self.currentTab = tabController.tabs.first! self.tabBarViewController = TabBarViewController(tabController: tabController) super.init(nibName: nil, bundle: nil) self.tabController.controllerDelegate = self addChild(toolbarController) addChild(findOnPageController) addChild(tabBarViewController) didChangeTab(currentTab) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func loadView() { browserView.toolbarView = toolbarController.toolbarView browserView.tabBarView = tabBarViewController.tabBarView if FindOnPageViewController.isEnabled() { browserView.findOnPageView = findOnPageController.findOnPageView // Find on page dismiss findOnPageController.findOnPageView.doneButton.addAction(UIAction(handler: { [unowned self] _ in browserView.setFindOnPageVisible(false, animated: true) }), for: .touchUpInside) } // Refresh button toolbarController.urlBar.refreshButton.addAction(UIAction(handler: { [unowned self] action in if self.webView.isLoading { self.webView.stopLoading() } else { self.webView.reload() } }), for: .touchUpInside) // Back button toolbarController.backButton.addAction(UIAction(handler: { [unowned self] _ in self.webView.goBack() }), for: .touchUpInside) // Forward button toolbarController.forwardButton.addAction(UIAction(handler: { [unowned self] _ in self.webView.goForward() }), for: .touchUpInside) // Share button toolbarController.shareButton.addAction(UIAction(handler: { [unowned self] _ in showShareSheetForCurrentURL(fromViewController: nil) }), for: .touchUpInside) // Script button toolbarController.scriptControllerIconView.addAction(UIAction(handler: { [unowned self] action in let hostOrigin = self.webView.url?.securityOrigin ?? "" let loadedScripts = currentTab.allowedScriptOrigins.union(currentTab.blockedScriptOrigins) let scriptViewController = ScriptPolicyViewController(policyManager: self.policyManager, hostOrigin: hostOrigin, loadedScripts: loadedScripts, scriptsAllowedForTab: currentTab.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: { [unowned self] _ in self.darkModeEnabled = !self.darkModeEnabled }), for: .touchUpInside) // Tabs button toolbarController.windowButton.addAction(UIAction(handler: { [unowned self] _ in let tabPickerController = TabPickerViewController() tabPickerController.delegate = self tabPickerController.title = "Tabs" tabPickerController.tabBarItem.image = UIImage(systemName: "rectangle.on.rectangle") tabPickerController.selectedTabIdentifier = currentTab.identifier tabPickerController.tabObserver = tabController.$tabs .receive(on: RunLoop.main) .sink(receiveValue: { (newTabs: [Tab]) in tabPickerController.setTabInfos(newTabs.map { $0.tabInfo }, forHost: TabPickerViewController.localHostIdentifier) }) // Set localhost tabs let tabInfos = tabController.tabs.map { $0.tabInfo } tabPickerController.setTabInfos(tabInfos, forHost: TabPickerViewController.localHostIdentifier) tabPickerController.selectedTabHost = TabPickerViewController.localHostIdentifier let remoteTabPickerController = TabPickerViewController() remoteTabPickerController.delegate = self remoteTabPickerController.title = "Remote Tabs" remoteTabPickerController.tabBarItem.image = UIImage(systemName: "icloud") remoteTabPickerController.newTabButton.isEnabled = false remoteTabPickerController.editButtonItem.isEnabled = false // Fetch tabs now AttractorServer.shared.getTabInfos { [weak remoteTabPickerController] result in guard let picker = remoteTabPickerController else { return } switch result { case .success(let tabInfos): tabInfos.forEach { (key: String, value: [TabInfo]) in picker.setTabInfos(value, forHost: key) } case .failure(let error): picker.displayedError = error } } let tabBarController = UITabBarController(nibName: nil, bundle: nil) tabBarController.viewControllers = [ UINavigationController(rootViewController: tabPickerController), UINavigationController(rootViewController: remoteTabPickerController) ] tabBarController.modalPresentationStyle = .popover tabBarController.popoverPresentationController?.sourceView = self.toolbarController.windowButton tabBarController.popoverPresentationController?.delegate = self self.present(tabBarController, animated: true, completion: nil) }), for: .touchUpInside) let newTabAction = UIAction { [unowned self] action in if let gestureRecognizer = action.sender as? UILongPressGestureRecognizer { if gestureRecognizer.state != .began { return } } self.createNewTab(withURL: nil) } if Self.longPressWindowButtonToMakeNewTab { // Long press window button to make new tab? let gestureRecognizer = UILongPressGestureRecognizer(action: newTabAction) toolbarController.windowButton.addGestureRecognizer(gestureRecognizer) } // New tab button toolbarController.newTabButton.addAction(newTabAction, for: .touchUpInside) // Error button toolbarController.urlBar.errorButton.addAction(UIAction(handler: { [unowned self] _ in let alert = UIAlertController(title: "Error", message: currentTab.loadError?.localizedDescription, preferredStyle: .actionSheet) alert.popoverPresentationController?.sourceView = self.toolbarController.urlBar.errorButton alert.addAction(UIAlertAction(title: "Reload", style: .destructive, handler: { _ in self.webView.reload() alert.dismiss(animated: true, completion: nil) })) alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { [unowned self] _ in alert.dismiss(animated: true, completion: nil) // Clears out the error state self.toolbarController.urlBar.loadProgress = .complete })) self.present(alert, animated: true, completion: nil) }), for: .touchUpInside) // Cancel button: hide autocomplete if applicable toolbarController.toolbarView.cancelButton.addAction(UIAction(handler: { [unowned self] _ in self.autocompleteViewController.view.isHidden = true }), for: .touchUpInside) // TextField delegate toolbarController.urlBar.delegate = self toolbarController.urlBar.textField.delegate = self // Autocomplete controller autocompleteViewController.delegate = self autocompleteViewController.view.isHidden = true // Font size adjust toolbarController.urlBar.documentButton.addAction(UIAction(handler: { [unowned self] _ in let documentControls = DocumentControlViewController(darkModeEnabled: currentTab.bridge.darkModeEnabled) documentControls.modalPresentationStyle = .popover documentControls.popoverPresentationController?.permittedArrowDirections = [ .down, .up ] documentControls.popoverPresentationController?.sourceView = toolbarController.urlBar.documentButton documentControls.popoverPresentationController?.delegate = self let numberFormatter = NumberFormatter() numberFormatter.numberStyle = .percent let label = documentControls.fontSizeAdjustView.labelView label.text = numberFormatter.string(for: currentTab.webView._viewScale) // Font size adjust documentControls.fontSizeAdjustView.decreaseSizeButton.addAction(UIAction(handler: { [unowned self] sender in self.decreaseSize(sender) label.text = numberFormatter.string(for: currentTab.webView._viewScale) }), for: .touchUpInside) documentControls.fontSizeAdjustView.increaseSizeButton.addAction(UIAction(handler: { [unowned self] sender in self.increaseSize(sender) label.text = numberFormatter.string(for: currentTab.webView._viewScale) }), for: .touchUpInside) // Find on page documentControls.findOnPageControlView.addAction(UIAction(handler: { [unowned self] _ in documentControls.dismiss(animated: true, completion: { [unowned self] in // Needs to happen after dismissal, otherwise the find navigator will pick up the document // controls as the divergent responder/deepest action responder (my bad...). if FindOnPageViewController.isEnabled() { browserView.setFindOnPageVisible(true, animated: true) } else if #available(iOS 16.0, *) { browserView.webView?.findInteraction?.presentFindNavigator(showingReplace: false) } }) }), for: .touchUpInside) // Navigation controls documentControls.navigationControlView.backButton.isEnabled = webView.canGoBack documentControls.navigationControlView.backButton.addAction(UIAction() { [unowned self] _ in webView.goBack() }, for: .touchUpInside) documentControls.observations.append(webView.observe(\.canGoBack, changeHandler: { (webView, _) in documentControls.navigationControlView.backButton.isEnabled = webView.canGoBack })) documentControls.navigationControlView.forwardButton.isEnabled = webView.canGoForward documentControls.navigationControlView.forwardButton.addAction(UIAction() { [unowned self] _ in webView.goForward() }, for: .touchUpInside) documentControls.observations.append(webView.observe(\.canGoForward, changeHandler: { (webView, _) in documentControls.navigationControlView.forwardButton.isEnabled = webView.canGoForward })) // Reader mode documentControls.readabilityView.addAction(UIAction { [unowned self] _ in documentControls.dismiss(animated: true, completion: nil) showReaderWindow() }, for: .touchUpInside) // Archive documentControls.archiveView.addAction(UIAction { [unowned self] _ in guard let currentURL = webView.url else { return } guard let archiveURL = URL(string: "https://archive.today/\(currentURL.absoluteString)") else { return } // Open in new tab: createNewTab(withURL: archiveURL) documentControls.dismiss(animated: true, completion: nil) }, for: .touchUpInside) // Dark mode documentControls.darkModeView.addAction(UIAction { [unowned self] _ in self.darkModeEnabled = !self.darkModeEnabled documentControls.dismiss(animated: true, completion: nil) }, for: .touchUpInside) // Email documentControls.emailView.addAction(UIAction { [unowned self] _ in queryPreferredEmailSharingRecipientForCurrentURL(fromViewController: documentControls) }, for: .touchUpInside) // Settings documentControls.settingsView.addAction(UIAction { [unowned self] _ in documentControls.dismiss(animated: false, completion: nil) showSettingsWindow() }, for: .touchUpInside) // Share documentControls.sharingView.addAction(UIAction { [unowned self] _ in showShareSheetForCurrentURL(fromViewController: documentControls) }, for: .touchUpInside) // History documentControls.historyView.addAction(UIAction { [unowned self] action in documentControls.dismiss(animated: false, completion: nil) showHistory(action) }, for: .touchUpInside) present(documentControls, animated: true, completion: nil) }), for: .touchUpInside) // Tab controller activeTabObservation = tabController.$activeTabIndex .receive(on: RunLoop.main) .sink(receiveValue: { [unowned self] (activeTab: Int) in if activeTab < tabController.tabs.count { let tab = tabController.tabs[activeTab] if currentTab != tab { currentTab = tab } } // Show tab bar view? self.updateTabBarVisibility() }) registerForTraitChanges([UITraitHorizontalSizeClass.self]) { [weak self] (traitEnvironment: Self, previousTraitCollection) in guard let self else { return } toolbarController.reconfigureButtons(for: traitCollection.horizontalSizeClass) } self.view = browserView } internal func updateTabBarVisibility() { browserView.tabBarViewVisible = tabController.tabs.count > 1 } internal func showShareSheetForCurrentURL(fromViewController: UIViewController?) { guard let url = self.webView.url else { return } let shareableURL = ShareableURL( url: url, title: webView.title ?? url.absoluteString, favicon: currentTab.favicon ) let activityController = UIActivityViewController(activityItems: [ shareableURL ], applicationActivities: nil) activityController.popoverPresentationController?.sourceView = toolbarController.shareButton if let fromViewController = fromViewController { fromViewController.dismiss(animated: false, completion: nil) } self.present(activityController, animated: true, completion: nil) } internal func queryPreferredEmailSharingRecipientForCurrentURL(fromViewController: UIViewController?) { if let fromViewController = fromViewController { fromViewController.dismiss(animated: false, completion: nil) } let actionSheet = UIAlertController(title: "Select Preferred Recipient", message: nil, preferredStyle: .actionSheet) for preferredRecipient in PreferredEmailSharingRecipient.allCases { actionSheet.addAction(UIAlertAction(title: preferredRecipient.rawValue, style: .default, handler: { [unowned self] action in actionSheet.dismiss(animated: true, completion: { [unowned self] in composeEmailForCurrentURL(preferredRecipientSelection: preferredRecipient) }) })) } actionSheet.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: { _ in actionSheet.dismiss(animated: true, completion: nil) })) actionSheet.popoverPresentationController?.sourceView = toolbarController.urlBar.documentButton present(actionSheet, animated: true, completion: nil) } internal func composeEmailForCurrentURL(preferredRecipientSelection: PreferredEmailSharingRecipient) { guard let url = self.webView.url else { return } let composeController = MFMailComposeViewController() composeController.setSubject(webView.title ?? url.absoluteString) composeController.setMessageBody(url.absoluteString, isHTML: false) composeController.mailComposeDelegate = self let preferredRecipient: String? = { preferredRecipientSelection in switch preferredRecipientSelection { case .readLater: return "read@buzzert.net" case .bookmark: return "bookmarks@buzzert.net" case .other: return nil } }(preferredRecipientSelection) if let preferredRecipient = preferredRecipient { composeController.setToRecipients([ preferredRecipient ]) } present(composeController, animated: true, completion: nil) } internal func showSettingsWindow() { #if targetEnvironment(macCatalyst) let userActivity = NSUserActivity(activityType: SessionActivityType.SettingsWindow.rawValue) UIApplication.shared.requestSceneSessionActivation(nil, userActivity: userActivity, options: .none, errorHandler: nil) #else let settingsVC = SettingsViewController(windowScene: view.window!.windowScene!) let wrapperNC = UINavigationController(rootViewController: settingsVC) present(wrapperNC, animated: true, completion: nil) #endif } internal func showReaderWindow() { currentTab.bridge.parseDocumentForReaderMode { [currentTab] string in DispatchQueue.main.async { if self.traitCollection.userInterfaceIdiom == .phone { let readableViewController = ReaderViewController(readableHTMLString: string, baseURL: currentTab.bridge.webView.url) readableViewController.title = currentTab.bridge.webView.title readableViewController.darkModeEnabled = currentTab.bridge.darkModeEnabled readableViewController.delegate = self let navigationController = UINavigationController(rootViewController: readableViewController) self.present(navigationController, animated: true, completion: nil) } else { let userActivity = NSUserActivity(activityType: SessionActivityType.ReaderWindow.rawValue) userActivity.title = currentTab.title userActivity.userInfo = [ ReaderUserActivityKeys.baseURL.rawValue : currentTab.bridge.webView.url ?? NSNull(), ReaderUserActivityKeys.htmlString.rawValue : string, ] let requestOptions = UIWindowScene.ActivationRequestOptions() requestOptions.preferredPresentationStyle = .prominent UIApplication.shared.requestSceneSessionActivation(nil, userActivity: userActivity, options: requestOptions) } } } } internal func showHistoryWindow() { let historyViewController = HistoryBrowserViewController { [unowned self] url in if currentTab.url == nil { currentTab.beginLoadingURL(url) } else { createNewTab(withURL: url) } presentedViewController?.dismiss(animated: true) } historyViewController.title = "History" historyViewController.navigationItem.rightBarButtonItem = UIBarButtonItem(systemItem: .done, primaryAction: UIAction { _ in // xxx: This is not the SwiftUI-y way to do this. historyViewController.dismiss(animated: true) }) historyViewController.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Clear", primaryAction: UIAction { [unowned self] action in let alertController = UIAlertController(title: "Clear History", message: "Are you sure you want to clear all history?", preferredStyle: .actionSheet) alertController.addAction(UIAlertAction(title: "Clear", style: .destructive, handler: { [unowned self] _ in BrowserHistory.shared.clearAllHistory() presentedViewController?.dismiss(animated: true) })) alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel)) alertController.popoverPresentationController?.sourceItem = action.presentationSourceItem presentedViewController?.present(alertController, animated: true) }) let navigationController = UINavigationController(rootViewController: historyViewController) present(navigationController, animated: true) } internal func updateLoadProgress(forWebView webView: WKWebView) { if let loadError = currentTab.loadError { toolbarController.urlBar.loadProgress = .error(error: loadError) } else if webView.estimatedProgress == 1.0 { toolbarController.urlBar.loadProgress = .complete } else if webView.isLoading { toolbarController.urlBar.loadProgress = .loading(progress: webView.estimatedProgress) } else { toolbarController.urlBar.loadProgress = .idle } } internal func updateTitleAndURL(forWebView webView: WKWebView) { guard let tab = tabController.tab(forWebView: webView) else { return } if webView == browserView.webView { browserView.titlebarView.setTitle(webView.title ?? "") if let urlString = webView.url?.absoluteString { toolbarController.urlBar.textField.text = urlString } else { toolbarController.urlBar.textField.text = "" } } // Figure out which tab this corresponds to if let tabIndex = tabController.tabs.firstIndex(of: tab) { tabBarViewController.tabBarView.reloadTab(atIndex: tabIndex) } } private func didChangeTab(_ tab: Tab) { if let activeIndex = tabController.tabs.firstIndex(of: tab) { tabController.activeTabIndex = activeIndex } currentTab.delegate = self let webView = currentTab.webView webView.allowsBackForwardNavigationGestures = true webView.navigationDelegate = self webView.uiDelegate = self // Change webView browserView.webView = webView findOnPageController.webView = webView // Autocomplete view browserView.autocompleteView = autocompleteViewController.collectionView // Color theme browserView.titlebarView.setColorTheme(tab.colorTheme) // Load progress updateLoadProgress(forWebView: webView) loadingObservation = webView.observe(\.estimatedProgress) { [unowned self] (webView, observedChange) in self.updateLoadProgress(forWebView: webView) } // Title observer updateTitleAndURL(forWebView: webView) // Back/forward observer toolbarController.backButton.isEnabled = webView.canGoBack backButtonObservation = webView.observe(\.canGoBack, changeHandler: { [unowned self] (webView, observedChange) in toolbarController.backButton.isEnabled = webView.canGoBack }) toolbarController.forwardButton.isEnabled = webView.canGoForward forwardButtonObservation = webView.observe(\.canGoForward, changeHandler: { [unowned self] (webView, observedChange) in toolbarController.forwardButton.isEnabled = webView.canGoForward }) // Secure content browserView.titlebarView.showsSecurityIndicator = webView.hasOnlySecureContent hasSecureContentObservation = webView.observe(\.hasOnlySecureContent, changeHandler: { [unowned self] (webView, observedChange) in browserView.titlebarView.showsSecurityIndicator = webView.hasOnlySecureContent }) // Script blocker button updateScriptBlockerButton() // Enforce dark mode setting currentTab.bridge.darkModeEnabled = toolbarController.darkModeEnabled // Blur url bar, if applicable toolbarController.urlBar.textField.resignFirstResponder() } override func target(forAction action: Selector, withSender sender: Any?) -> Any? { var findActions: [Selector] = [] if #available(macCatalyst 16.0, iOS 16.0, *) { findActions = [ #selector(UIResponder.find(_:)), #selector(UIResponder.findNext(_:)), #selector(UIResponder.findPrevious(_:)), #selector(UIResponder.findAndReplace(_:)), #selector(UIResponder.useSelectionForFind(_:)), ] } if findActions.contains(where: { $0 == action }) { return webView.target(forAction: action, withSender: sender) } return super.target(forAction: action, withSender: sender) } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) becomeFirstResponder() } internal func updateScriptBlockerButton() { var numBlockedScripts: Int = currentTab.blockedScriptOrigins.count if currentTab.url != nil, currentTab.javaScriptEnabled == false { // Because the page is blocked too, notify. numBlockedScripts += 1 } var policy: ScriptPolicy? = nil if let url = webView.url, let host = url.host { policy = policyManager.scriptPolicy(forOrigin: host) } let iconView = toolbarController.scriptControllerIconView iconView.shieldsDown = currentTab.javaScriptEnabled // iconView.setBlockedScriptsNumber(numBlockedScripts) if let policy = policy { iconView.currentPolicy = policy } iconView.isEnabled = (webView.url != nil) } @discardableResult public func createNewTab(withURL url: URL?, loadInBackground: Bool = false) -> Tab { let newTab = tabController.createNewTab(url: url) if !loadInBackground { currentTab = newTab if url == nil && traitCollection.userInterfaceIdiom == .mac { self.toolbarController.urlBar.textField.becomeFirstResponder() } } else { // Send this message to get it to load NOW, instead of waiting for it to show up // in the view hierarchy. currentTab.webView.didMoveToWindow() // Update tab bar now updateTabBarVisibility() } return newTab } } extension BrowserViewController: UIPopoverPresentationControllerDelegate { func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle { // Forces popovers to present on iPhone return .none } } extension BrowserViewController: ScriptPolicyViewControllerDelegate { func didChangeScriptPolicy() { currentTab.bridge.policyDataSourceDidChange() webView.reload() } func setScriptsEnabledForTab(_ enabled: Bool) { currentTab.javaScriptEnabled = enabled toolbarController.scriptControllerIconView.shieldsDown = enabled } } extension BrowserViewController: AutocompleteViewControllerDelegate { func autocompleteController(_: AutocompleteViewController, didSelectHistoryItem item: HistoryItem) { currentTab.beginLoadingURL(item.url) autocompleteViewController.view.isHidden = true } } extension BrowserViewController: TabDelegate { func didBlockScriptOrigin(_ origin: String, forTab: Tab) { updateScriptBlockerButton() } } extension BrowserViewController: TabPickerViewControllerDelegate { func tabPicker(_ picker: TabPickerViewController, createNewTabWithURL url: URL?) { currentTab = tabController.createNewTab(url: url) picker.dismiss(animated: true) } func tabPicker(_ picker: TabPickerViewController, tabInfoForIdentifier identifier: UUID) -> TabInfo { guard let tab = tabController.tab(forIdentifier: identifier) else { fatalError() } return tab.tabInfo } func tabPicker(_ picker: TabPickerViewController, didSelectTabInfo info: TabInfo, fromHost host: String) { var tab: Tab? if host == TabPickerViewController.localHostIdentifier { tab = tabController.tab(forIdentifier: info.identifier) } else if let urlString = info.urlString { tab = tabController.createNewTab(url: URL(string: urlString)) } guard let tab else { return } currentTab = tab picker.dismiss(animated: true, completion: nil) } func tabPicker(_ picker: TabPickerViewController, closeTabWithIdentifier tabIdentifier: UUID) { guard let tab = tabController.tab(forIdentifier: tabIdentifier) else { return } tabController.closeTab(tab) // Dismiss picker if current tab is closed using the picker if tab == currentTab { picker.dismiss(animated: true, completion: nil) } } } extension BrowserViewController: UITextFieldDelegate { func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { if let text = textField.text { let matches = BrowserHistory.shared.visitedToplevelHistoryItems(matching: text) autocompleteViewController.historyItems = matches autocompleteViewController.view.isHidden = matches.count == 0 } return true } func textFieldShouldReturn(_ textField: UITextField) -> Bool { if let text = textField.text?.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) { // Dumb rules for stuff that "looks like" a URL if !text.contains(" "), text.components(separatedBy: ".").count > 1, var url = URL(string: text) { if url.scheme == nil { let urlString = "http://\(text)" if let fixedURL = URL(string: urlString) { url = fixedURL } } currentTab.beginLoadingURL(url) } else { let searchURL = Settings.shared.searchProvider.provider().searchURLWithQuery(text) currentTab.beginLoadingURL(searchURL) } textField.resignFirstResponder() } return false } override func textFieldShouldEndEditing(_ textField: UITextField) -> Bool { true } func textFieldDidEndEditing(_ textField: UITextField) { if !changingFocusToAutocompleteController { autocompleteViewController.view.isHidden = true } } } extension BrowserViewController: ReaderViewControllerDelegate { func readerViewController(_ reader: ReaderViewController, didRequestNavigationToURL navigationURL: URL) { currentTab.beginLoadingURL(navigationURL) } } extension BrowserViewController: URLBarDelegate { func urlBarRequestedFocusEscape(_ urlBar: URLBar) { changingFocusToAutocompleteController = true _ = self.autocompleteViewController.becomeFirstResponder() changingFocusToAutocompleteController = false } } extension BrowserViewController: MFMailComposeViewControllerDelegate { func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) { controller.dismiss(animated: true, completion: nil) } } extension BrowserViewController: TabControllerDelegate { func tabController(_ controller: TabController, didUpdateTitle: String, forTab tab: Tab) { updateTitleAndURL(forWebView: tab.webView) // Fetch favicon in background, if applicable if tab.favicon == nil, let url = tab.webView.url { tab.updateFaviconForURL(url) } } func tabController(_ controller: TabController, didUpdateFavicon: UIImage?, forTab tab: Tab) { updateTitleAndURL(forWebView: tab.webView) } }