Autocomplete UI

This commit is contained in:
James Magahern
2020-09-21 17:56:22 -07:00
parent 7f2cd23889
commit e2d5dbee59
5 changed files with 155 additions and 5 deletions

View File

@@ -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<Section, HistoryItem>
init() {
let listConfiguration = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
let listLayout = UICollectionViewCompositionalLayout.list(using: listConfiguration)
collectionView = UICollectionView(frame: .zero, collectionViewLayout: listLayout)
let cellRegistry = UICollectionView.CellRegistration<UICollectionViewListCell, HistoryItem> { (cell, indexPath, item) in
var config = cell.defaultContentConfiguration()
config.text = item.title
config.secondaryText = item.url.absoluteString
cell.contentConfiguration = config
}
dataSource = UICollectionViewDiffableDataSource<Section, HistoryItem>(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)
}
}
}

View File

@@ -62,11 +62,15 @@ class BrowserHistory
let topLevelURL = item.url.topLevelURL() let topLevelURL = item.url.topLevelURL()
var topLevelItem = topLevelItems[topLevelURL] ?? (item, 0) var topLevelItem = topLevelItems[topLevelURL] ?? (item, 0)
topLevelItem.0.url = topLevelURL topLevelItem.0.url = topLevelURL
if item.url.path == "/" || item.url.path == "" {
topLevelItem.0.title = item.title
}
topLevelItem.1 += 1
topLevelItems[topLevelURL] = topLevelItem topLevelItems[topLevelURL] = topLevelItem
} }
return topLevelItems.values.map { return $0.0 }.sorted { (item1, item2) -> Bool in 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 { public func topLevelURL() -> URL {
if var components = URLComponents(url: self, resolvingAgainstBaseURL: false) { if var components = URLComponents(url: self, resolvingAgainstBaseURL: false) {
components.path = "" components.path = ""
components.query = "" components.query = nil
components.queryItems = [] components.queryItems = nil
components.fragment = nil
if let url = components.url { if let url = components.url {
return url return url
} }

View File

@@ -17,6 +17,10 @@ class BrowserView: UIView
didSet { addSubview(toolbarView!) } didSet { addSubview(toolbarView!) }
} }
var autocompleteView: UICollectionView? {
didSet { addSubview(autocompleteView!) }
}
var webView: WKWebView? { var webView: WKWebView? {
didSet { didSet {
oldValue?.removeFromSuperview() oldValue?.removeFromSuperview()
@@ -104,5 +108,29 @@ class BrowserView: UIView
webView.scrollView.layer.masksToBounds = false // allow content to draw under titlebar/toolbar webView.scrollView.layer.masksToBounds = false // allow content to draw under titlebar/toolbar
webView.frame = bounds.inset(by: webViewContentInset) 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
)
}
}
}
} }
} }

View File

@@ -10,7 +10,8 @@ import UniformTypeIdentifiers
class BrowserViewController: UIViewController, WKNavigationDelegate, WKUIDelegate, class BrowserViewController: UIViewController, WKNavigationDelegate, WKUIDelegate,
UITextFieldDelegate, ScriptPolicyViewControllerDelegate, UITextFieldDelegate, ScriptPolicyViewControllerDelegate,
UIPopoverPresentationControllerDelegate, TabDelegate, TabPickerViewControllerDelegate UIPopoverPresentationControllerDelegate, TabDelegate, TabPickerViewControllerDelegate,
AutocompleteViewControllerDelegate
{ {
let browserView = BrowserView() let browserView = BrowserView()
var tab: Tab { didSet { didChangeTab(tab) } } var tab: Tab { didSet { didChangeTab(tab) } }
@@ -19,6 +20,8 @@ class BrowserViewController: UIViewController, WKNavigationDelegate, WKUIDelegat
private let tabController = TabController() private let tabController = TabController()
private let toolbarController = ToolbarViewController() private let toolbarController = ToolbarViewController()
private let autocompleteViewController = AutocompleteViewController()
private var policyManager: ResourcePolicyManager { tabController.policyManager } private var policyManager: ResourcePolicyManager { tabController.policyManager }
override var canBecomeFirstResponder: Bool { true } override var canBecomeFirstResponder: Bool { true }
@@ -158,9 +161,18 @@ class BrowserViewController: UIViewController, WKNavigationDelegate, WKUIDelegat
self.present(alert, animated: true, completion: nil) self.present(alert, animated: true, completion: nil)
}), for: .touchUpInside) }), 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 // TextField delegate
toolbarController.urlBar.textField.delegate = self toolbarController.urlBar.textField.delegate = self
// Autocomplete controller
autocompleteViewController.delegate = self
autocompleteViewController.view.isHidden = true
self.view = browserView self.view = browserView
} }
@@ -195,6 +207,9 @@ class BrowserViewController: UIViewController, WKNavigationDelegate, WKUIDelegat
// Change webView // Change webView
browserView.webView = webView browserView.webView = webView
// Autocomplete view
browserView.autocompleteView = autocompleteViewController.collectionView
// Load progress // Load progress
updateLoadProgress(forWebView: webView) updateLoadProgress(forWebView: webView)
loadingObservation = webView.observe(\.estimatedProgress) { [unowned self] (webView, observedChange) in loadingObservation = webView.observe(\.estimatedProgress) { [unowned self] (webView, observedChange) in
@@ -272,6 +287,9 @@ class BrowserViewController: UIViewController, WKNavigationDelegate, WKUIDelegat
tab.blockedScriptOrigins.removeAll() tab.blockedScriptOrigins.removeAll()
updateScriptBlockerButton() updateScriptBlockerButton()
// Blur url bar if applicable
toolbarController.urlBar.textField.resignFirstResponder()
updateTitleAndURL(forWebView: webView) updateTitleAndURL(forWebView: webView)
if let url = webView.url { 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 { func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if let text = textField.text { if let text = textField.text {
let matches = BrowserHistory.shared.visitedToplevelHistoryItems(matching: text) let matches = BrowserHistory.shared.visitedToplevelHistoryItems(matching: text)
print(matches) autocompleteViewController.historyItems = matches
autocompleteViewController.view.isHidden = matches.count == 0
} }
return true return true
@@ -358,9 +378,14 @@ class BrowserViewController: UIViewController, WKNavigationDelegate, WKUIDelegat
textField.resignFirstResponder() textField.resignFirstResponder()
} }
return false return false
} }
func textFieldDidEndEditing(_ textField: UITextField) {
autocompleteViewController.view.isHidden = true
}
// MARK: Tab Delegate // MARK: Tab Delegate
func didBlockScriptOrigin(_ origin: String, forTab: Tab) { func didBlockScriptOrigin(_ origin: String, forTab: Tab) {
@@ -396,4 +421,11 @@ class BrowserViewController: UIViewController, WKNavigationDelegate, WKUIDelegat
tab.javaScriptEnabled = enabled tab.javaScriptEnabled = enabled
toolbarController.scriptControllerIconView.shieldsDown = enabled toolbarController.scriptControllerIconView.shieldsDown = enabled
} }
// MARK: Autocomplete Controller Delegate
func autocompleteController(_: AutocompleteViewController, didSelectHistoryItem item: HistoryItem) {
tab.beginLoadingURL(item.url)
autocompleteViewController.view.isHidden = true
}
} }

View File

@@ -36,6 +36,7 @@
1ADFF4CB24CB8278006DC7AE /* ScriptControllerIconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ADFF4CA24CB8278006DC7AE /* ScriptControllerIconView.swift */; }; 1ADFF4CB24CB8278006DC7AE /* ScriptControllerIconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ADFF4CA24CB8278006DC7AE /* ScriptControllerIconView.swift */; };
1ADFF4CD24CBB0C8006DC7AE /* ScriptPolicyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ADFF4CC24CBB0C8006DC7AE /* ScriptPolicyViewController.swift */; }; 1ADFF4CD24CBB0C8006DC7AE /* ScriptPolicyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ADFF4CC24CBB0C8006DC7AE /* ScriptPolicyViewController.swift */; };
1ADFF4D024CBBCD1006DC7AE /* ScriptPolicyControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ADFF4CF24CBBCD1006DC7AE /* ScriptPolicyControl.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 */; }; CD853BCE24E7763900D2BDCC /* BrowserHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD853BCD24E7763900D2BDCC /* BrowserHistory.swift */; };
CD853BD124E778B800D2BDCC /* History.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = CD853BCF24E778B800D2BDCC /* History.xcdatamodeld */; }; CD853BD124E778B800D2BDCC /* History.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = CD853BCF24E778B800D2BDCC /* History.xcdatamodeld */; };
CD853BD424E77BF900D2BDCC /* HistoryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD853BD324E77BF900D2BDCC /* HistoryItem.swift */; }; 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 = "<group>"; }; 1ADFF4CA24CB8278006DC7AE /* ScriptControllerIconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScriptControllerIconView.swift; sourceTree = "<group>"; };
1ADFF4CC24CBB0C8006DC7AE /* ScriptPolicyViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScriptPolicyViewController.swift; sourceTree = "<group>"; }; 1ADFF4CC24CBB0C8006DC7AE /* ScriptPolicyViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScriptPolicyViewController.swift; sourceTree = "<group>"; };
1ADFF4CF24CBBCD1006DC7AE /* ScriptPolicyControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScriptPolicyControl.swift; sourceTree = "<group>"; }; 1ADFF4CF24CBBCD1006DC7AE /* ScriptPolicyControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScriptPolicyControl.swift; sourceTree = "<group>"; };
CD7A8914251975B70075991E /* AutocompleteViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutocompleteViewController.swift; sourceTree = "<group>"; };
CD853BCD24E7763900D2BDCC /* BrowserHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserHistory.swift; sourceTree = "<group>"; }; CD853BCD24E7763900D2BDCC /* BrowserHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserHistory.swift; sourceTree = "<group>"; };
CD853BD024E778B800D2BDCC /* History.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = History.xcdatamodel; sourceTree = "<group>"; }; CD853BD024E778B800D2BDCC /* History.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = History.xcdatamodel; sourceTree = "<group>"; };
CD853BD324E77BF900D2BDCC /* HistoryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryItem.swift; sourceTree = "<group>"; }; CD853BD324E77BF900D2BDCC /* HistoryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryItem.swift; sourceTree = "<group>"; };
@@ -192,6 +194,7 @@
children = ( children = (
1ADFF45F24C7DE53006DC7AE /* AppDelegate.swift */, 1ADFF45F24C7DE53006DC7AE /* AppDelegate.swift */,
1ADFF46124C7DE53006DC7AE /* SceneDelegate.swift */, 1ADFF46124C7DE53006DC7AE /* SceneDelegate.swift */,
CD7A89132519759D0075991E /* Autocomplete */,
1ADFF47A24C7E176006DC7AE /* Backend */, 1ADFF47A24C7E176006DC7AE /* Backend */,
1ADFF47724C7DFE8006DC7AE /* Browser View */, 1ADFF47724C7DFE8006DC7AE /* Browser View */,
1A03810E24E71CCA00826501 /* Common UI */, 1A03810E24E71CCA00826501 /* Common UI */,
@@ -285,6 +288,14 @@
path = "Script Policy UI"; path = "Script Policy UI";
sourceTree = "<group>"; sourceTree = "<group>";
}; };
CD7A89132519759D0075991E /* Autocomplete */ = {
isa = PBXGroup;
children = (
CD7A8914251975B70075991E /* AutocompleteViewController.swift */,
);
path = Autocomplete;
sourceTree = "<group>";
};
CD853BD224E77BEF00D2BDCC /* History */ = { CD853BD224E77BEF00D2BDCC /* History */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -414,6 +425,7 @@
CD853BCE24E7763900D2BDCC /* BrowserHistory.swift in Sources */, CD853BCE24E7763900D2BDCC /* BrowserHistory.swift in Sources */,
1A03810B24E71C5600826501 /* ToolbarButtonContainerView.swift in Sources */, 1A03810B24E71C5600826501 /* ToolbarButtonContainerView.swift in Sources */,
1ADFF4CB24CB8278006DC7AE /* ScriptControllerIconView.swift in Sources */, 1ADFF4CB24CB8278006DC7AE /* ScriptControllerIconView.swift in Sources */,
CD7A8915251975B70075991E /* AutocompleteViewController.swift in Sources */,
1AB88EFD24D3BA560006F850 /* TabController.swift in Sources */, 1AB88EFD24D3BA560006F850 /* TabController.swift in Sources */,
1ADFF4C324CA6AF6006DC7AE /* Geometry.swift in Sources */, 1ADFF4C324CA6AF6006DC7AE /* Geometry.swift in Sources */,
1ADFF4C924CA793E006DC7AE /* ToolbarViewController.swift in Sources */, 1ADFF4C924CA793E006DC7AE /* ToolbarViewController.swift in Sources */,