diff --git a/App/Autocomplete/AutocompleteViewController.swift b/App/Autocomplete/AutocompleteViewController.swift new file mode 100644 index 0000000..c3ec030 --- /dev/null +++ b/App/Autocomplete/AutocompleteViewController.swift @@ -0,0 +1,73 @@ +// +// AutocompleteViewController.swift +// App +// +// Created by James Magahern on 9/21/20. +// + +import UIKit + +protocol AutocompleteViewControllerDelegate: class { + func autocompleteController(_: AutocompleteViewController, didSelectHistoryItem: HistoryItem) +} + +class AutocompleteViewController: UIViewController, UICollectionViewDelegate +{ + public var historyItems: [HistoryItem] = [] { + didSet { + var snapshot = dataSource.snapshot() + snapshot.deleteAllItems() + snapshot.appendSections([ .HistoryItems ]) + snapshot.appendItems(historyItems, toSection: .HistoryItems) + dataSource.apply(snapshot, animatingDifferences: false) + } + } + + public weak var delegate: AutocompleteViewControllerDelegate? + + private enum Section: Int { + case HistoryItems + } + + public let collectionView: UICollectionView + private let dataSource: UICollectionViewDiffableDataSource + + init() { + let listConfiguration = UICollectionLayoutListConfiguration(appearance: .insetGrouped) + let listLayout = UICollectionViewCompositionalLayout.list(using: listConfiguration) + collectionView = UICollectionView(frame: .zero, collectionViewLayout: listLayout) + + let cellRegistry = UICollectionView.CellRegistration { (cell, indexPath, item) in + var config = cell.defaultContentConfiguration() + config.text = item.title + config.secondaryText = item.url.absoluteString + cell.contentConfiguration = config + } + + dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView, cellProvider: + { (collectionView, indexPath, item) -> UICollectionViewCell? in + collectionView.dequeueConfiguredReusableCell(using: cellRegistry, for: indexPath, item: item) + }) + + super.init(nibName: nil, bundle: nil) + + collectionView.delegate = self + view.backgroundColor = .systemBackground + } + + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + override func loadView() { + self.view = collectionView + } + + // MARK: UICollectionViewDelegate + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + collectionView.deselectItem(at: indexPath, animated: true) + + if let item = dataSource.itemIdentifier(for: indexPath) { + delegate?.autocompleteController(self, didSelectHistoryItem: item) + } + } +} diff --git a/App/Backend/History/BrowserHistory.swift b/App/Backend/History/BrowserHistory.swift index 2cc7e95..b5cbea2 100644 --- a/App/Backend/History/BrowserHistory.swift +++ b/App/Backend/History/BrowserHistory.swift @@ -62,11 +62,15 @@ class BrowserHistory let topLevelURL = item.url.topLevelURL() var topLevelItem = topLevelItems[topLevelURL] ?? (item, 0) topLevelItem.0.url = topLevelURL + if item.url.path == "/" || item.url.path == "" { + topLevelItem.0.title = item.title + } + topLevelItem.1 += 1 topLevelItems[topLevelURL] = topLevelItem } return topLevelItems.values.map { return $0.0 }.sorted { (item1, item2) -> Bool in - return topLevelItems[item1.url]!.1 < topLevelItems[item2.url]!.1 + return topLevelItems[item1.url]!.1 > topLevelItems[item2.url]!.1 } } } @@ -76,8 +80,9 @@ extension URL public func topLevelURL() -> URL { if var components = URLComponents(url: self, resolvingAgainstBaseURL: false) { components.path = "" - components.query = "" - components.queryItems = [] + components.query = nil + components.queryItems = nil + components.fragment = nil if let url = components.url { return url } diff --git a/App/Browser View/BrowserView.swift b/App/Browser View/BrowserView.swift index f572590..92ec30e 100644 --- a/App/Browser View/BrowserView.swift +++ b/App/Browser View/BrowserView.swift @@ -17,6 +17,10 @@ class BrowserView: UIView didSet { addSubview(toolbarView!) } } + var autocompleteView: UICollectionView? { + didSet { addSubview(autocompleteView!) } + } + var webView: WKWebView? { didSet { oldValue?.removeFromSuperview() @@ -104,5 +108,29 @@ class BrowserView: UIView webView.scrollView.layer.masksToBounds = false // allow content to draw under titlebar/toolbar webView.frame = bounds.inset(by: webViewContentInset) } + + // Autocomplete view + if let autocompleteView = autocompleteView { + // Compact: autocomplete view takes the space of the webview + autocompleteView.frame = bounds.inset(by: webViewContentInset) + if traitCollection.horizontalSizeClass == .regular { + // Regular: shows up just underneath the url bar + autocompleteView.layer.shadowColor = UIColor.black.cgColor + autocompleteView.layer.shadowOffset = CGSize(width: 0.0, height: 1.0) + autocompleteView.layer.shadowRadius = 3.0 + autocompleteView.layer.shadowOpacity = 0.8 + autocompleteView.layer.cornerRadius = 8.0 + + if let toolbarView = toolbarView, let urlBar = toolbarView.urlBar { + let urlFrame = self.convert(urlBar.frame, from: urlBar.superview) + autocompleteView.frame = CGRect( + x: urlFrame.minX, + y: toolbarView.frame.maxY + 3.0, + width: urlFrame.width, + height: bounds.height / 2.5 + ) + } + } + } } } diff --git a/App/Browser View/BrowserViewController.swift b/App/Browser View/BrowserViewController.swift index 65d48da..abc9a1a 100644 --- a/App/Browser View/BrowserViewController.swift +++ b/App/Browser View/BrowserViewController.swift @@ -10,7 +10,8 @@ import UniformTypeIdentifiers class BrowserViewController: UIViewController, WKNavigationDelegate, WKUIDelegate, UITextFieldDelegate, ScriptPolicyViewControllerDelegate, - UIPopoverPresentationControllerDelegate, TabDelegate, TabPickerViewControllerDelegate + UIPopoverPresentationControllerDelegate, TabDelegate, TabPickerViewControllerDelegate, + AutocompleteViewControllerDelegate { let browserView = BrowserView() var tab: Tab { didSet { didChangeTab(tab) } } @@ -19,6 +20,8 @@ class BrowserViewController: UIViewController, WKNavigationDelegate, WKUIDelegat private let tabController = TabController() private let toolbarController = ToolbarViewController() + private let autocompleteViewController = AutocompleteViewController() + private var policyManager: ResourcePolicyManager { tabController.policyManager } override var canBecomeFirstResponder: Bool { true } @@ -158,9 +161,18 @@ class BrowserViewController: UIViewController, WKNavigationDelegate, WKUIDelegat 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.textField.delegate = self + // Autocomplete controller + autocompleteViewController.delegate = self + autocompleteViewController.view.isHidden = true + self.view = browserView } @@ -195,6 +207,9 @@ class BrowserViewController: UIViewController, WKNavigationDelegate, WKUIDelegat // Change webView browserView.webView = webView + // Autocomplete view + browserView.autocompleteView = autocompleteViewController.collectionView + // Load progress updateLoadProgress(forWebView: webView) loadingObservation = webView.observe(\.estimatedProgress) { [unowned self] (webView, observedChange) in @@ -272,6 +287,9 @@ class BrowserViewController: UIViewController, WKNavigationDelegate, WKUIDelegat tab.blockedScriptOrigins.removeAll() updateScriptBlockerButton() + // Blur url bar if applicable + toolbarController.urlBar.textField.resignFirstResponder() + updateTitleAndURL(forWebView: webView) if let url = webView.url { @@ -327,7 +345,9 @@ class BrowserViewController: UIViewController, WKNavigationDelegate, WKUIDelegat func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { if let text = textField.text { let matches = BrowserHistory.shared.visitedToplevelHistoryItems(matching: text) - print(matches) + autocompleteViewController.historyItems = matches + + autocompleteViewController.view.isHidden = matches.count == 0 } return true @@ -358,9 +378,14 @@ class BrowserViewController: UIViewController, WKNavigationDelegate, WKUIDelegat textField.resignFirstResponder() } + return false } + func textFieldDidEndEditing(_ textField: UITextField) { + autocompleteViewController.view.isHidden = true + } + // MARK: Tab Delegate func didBlockScriptOrigin(_ origin: String, forTab: Tab) { @@ -396,4 +421,11 @@ class BrowserViewController: UIViewController, WKNavigationDelegate, WKUIDelegat tab.javaScriptEnabled = enabled toolbarController.scriptControllerIconView.shieldsDown = enabled } + + // MARK: Autocomplete Controller Delegate + + func autocompleteController(_: AutocompleteViewController, didSelectHistoryItem item: HistoryItem) { + tab.beginLoadingURL(item.url) + autocompleteViewController.view.isHidden = true + } } diff --git a/SBrowser.xcodeproj/project.pbxproj b/SBrowser.xcodeproj/project.pbxproj index c594edb..87f2ea6 100644 --- a/SBrowser.xcodeproj/project.pbxproj +++ b/SBrowser.xcodeproj/project.pbxproj @@ -36,6 +36,7 @@ 1ADFF4CB24CB8278006DC7AE /* ScriptControllerIconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ADFF4CA24CB8278006DC7AE /* ScriptControllerIconView.swift */; }; 1ADFF4CD24CBB0C8006DC7AE /* ScriptPolicyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ADFF4CC24CBB0C8006DC7AE /* ScriptPolicyViewController.swift */; }; 1ADFF4D024CBBCD1006DC7AE /* ScriptPolicyControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ADFF4CF24CBBCD1006DC7AE /* ScriptPolicyControl.swift */; }; + CD7A8915251975B70075991E /* AutocompleteViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD7A8914251975B70075991E /* AutocompleteViewController.swift */; }; CD853BCE24E7763900D2BDCC /* BrowserHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD853BCD24E7763900D2BDCC /* BrowserHistory.swift */; }; CD853BD124E778B800D2BDCC /* History.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = CD853BCF24E778B800D2BDCC /* History.xcdatamodeld */; }; CD853BD424E77BF900D2BDCC /* HistoryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD853BD324E77BF900D2BDCC /* HistoryItem.swift */; }; @@ -104,6 +105,7 @@ 1ADFF4CA24CB8278006DC7AE /* ScriptControllerIconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScriptControllerIconView.swift; sourceTree = ""; }; 1ADFF4CC24CBB0C8006DC7AE /* ScriptPolicyViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScriptPolicyViewController.swift; sourceTree = ""; }; 1ADFF4CF24CBBCD1006DC7AE /* ScriptPolicyControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScriptPolicyControl.swift; sourceTree = ""; }; + CD7A8914251975B70075991E /* AutocompleteViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutocompleteViewController.swift; sourceTree = ""; }; CD853BCD24E7763900D2BDCC /* BrowserHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserHistory.swift; sourceTree = ""; }; CD853BD024E778B800D2BDCC /* History.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = History.xcdatamodel; sourceTree = ""; }; CD853BD324E77BF900D2BDCC /* HistoryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryItem.swift; sourceTree = ""; }; @@ -192,6 +194,7 @@ children = ( 1ADFF45F24C7DE53006DC7AE /* AppDelegate.swift */, 1ADFF46124C7DE53006DC7AE /* SceneDelegate.swift */, + CD7A89132519759D0075991E /* Autocomplete */, 1ADFF47A24C7E176006DC7AE /* Backend */, 1ADFF47724C7DFE8006DC7AE /* Browser View */, 1A03810E24E71CCA00826501 /* Common UI */, @@ -285,6 +288,14 @@ path = "Script Policy UI"; sourceTree = ""; }; + CD7A89132519759D0075991E /* Autocomplete */ = { + isa = PBXGroup; + children = ( + CD7A8914251975B70075991E /* AutocompleteViewController.swift */, + ); + path = Autocomplete; + sourceTree = ""; + }; CD853BD224E77BEF00D2BDCC /* History */ = { isa = PBXGroup; children = ( @@ -414,6 +425,7 @@ CD853BCE24E7763900D2BDCC /* BrowserHistory.swift in Sources */, 1A03810B24E71C5600826501 /* ToolbarButtonContainerView.swift in Sources */, 1ADFF4CB24CB8278006DC7AE /* ScriptControllerIconView.swift in Sources */, + CD7A8915251975B70075991E /* AutocompleteViewController.swift in Sources */, 1AB88EFD24D3BA560006F850 /* TabController.swift in Sources */, 1ADFF4C324CA6AF6006DC7AE /* Geometry.swift in Sources */, 1ADFF4C924CA793E006DC7AE /* ToolbarViewController.swift in Sources */,