diff --git a/App/Browser View/BrowserViewController.swift b/App/Browser View/BrowserViewController.swift index 7bf3e66..58f447d 100644 --- a/App/Browser View/BrowserViewController.swift +++ b/App/Browser View/BrowserViewController.swift @@ -137,9 +137,15 @@ class BrowserViewController: UIViewController // Tabs button toolbarController.windowButton.addAction(UIAction(handler: { [unowned self] _ in - let tabPickerController = TabPickerViewController(tabController: self.tabController) + let tabPickerController = TabPickerViewController() tabPickerController.delegate = self - tabPickerController.selectedTab = self.tab + tabPickerController.selectedTabIdentifier = self.tab.identifier + tabPickerController.tabIdentifiers = tabController.tabs.map { $0.identifier } + tabPickerController.tabObserver = tabController.$tabs + .receive(on: RunLoop.main) + .sink(receiveValue: { (newTabs: [Tab]) in + tabPickerController.tabIdentifiers = newTabs.map { $0.identifier } + }) let navController = UINavigationController(rootViewController: tabPickerController) navController.modalPresentationStyle = .popover @@ -611,12 +617,28 @@ extension BrowserViewController: TabDelegate extension BrowserViewController: TabPickerViewControllerDelegate { - func tabPicker(_ picker: TabPickerViewController, didSelectTab tab: Tab) { + func tabPicker(_ picker: TabPickerViewController, createNewTabWithURL url: URL?) { + self.tab = 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, didSelectTabIdentifier tabIdentifier: UUID) { + guard let tab = tabController.tab(forIdentifier: tabIdentifier) else { return } + self.tab = tab picker.dismiss(animated: true, completion: nil) } - func tabPicker(_ picker: TabPickerViewController, willCloseTab tab: Tab) { + 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 == self.tab { picker.dismiss(animated: true, completion: nil) diff --git a/App/Tabs/Tab.swift b/App/Tabs/Tab.swift index d5b8c99..2b4f7cc 100644 --- a/App/Tabs/Tab.swift +++ b/App/Tabs/Tab.swift @@ -17,6 +17,17 @@ class Tab: NSObject, SBRProcessBundleBridgeDelegate { public weak var delegate: TabDelegate? + public var tabInfo: TabInfo { + get { + TabInfo( + title: loadedWebView?.title, + url: loadedWebView?.url ?? self.homeURL, + favicon: self.favicon, + identifier: self.identifier + ) + } + } + public let homeURL: URL? public let bridge: ProcessBundleBridge public var webView: WKWebView { @@ -33,8 +44,8 @@ class Tab: NSObject, SBRProcessBundleBridgeDelegate 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 title: String? { get { tabInfo.title } } + public var url: URL? { get { tabInfo.url } } public var javaScriptEnabled: Bool = false { didSet { bridge.allowAllScripts = javaScriptEnabled } diff --git a/App/Tabs/TabBarView.swift b/App/Tabs/TabBarView.swift index c82c685..c1afdf6 100644 --- a/App/Tabs/TabBarView.swift +++ b/App/Tabs/TabBarView.swift @@ -174,7 +174,7 @@ class TabBarView: UIView } else { let newTabView = makeTabView(withIdentifier: identifier) if animated { newTabView.collapsed = true } - if i < tabViews.count { + if i > 0 && i < tabViews.count { tabViews.insert(newTabView, at: i - 1) } else { tabViews.append(newTabView) diff --git a/App/Tabs/TabInfo.swift b/App/Tabs/TabInfo.swift new file mode 100644 index 0000000..8b8d45c --- /dev/null +++ b/App/Tabs/TabInfo.swift @@ -0,0 +1,21 @@ +// +// TabInfo.swift +// App +// +// Created by James Magahern on 8/5/22. +// + +import Foundation +import Combine + +struct TabInfo +{ + public var title: String? + public var url: URL? + public var favicon: UIImage? + public var identifier = UUID() + + public static func ==(lhs: TabInfo, rhs: TabInfo) -> Bool { + return lhs.identifier == rhs.identifier + } +} diff --git a/App/Tabs/TabPickerViewController.swift b/App/Tabs/TabPickerViewController.swift index e9b2ce4..8f537ad 100644 --- a/App/Tabs/TabPickerViewController.swift +++ b/App/Tabs/TabPickerViewController.swift @@ -6,25 +6,83 @@ // import UIKit +import Combine protocol TabPickerViewControllerDelegate: AnyObject { - func tabPicker(_ picker: TabPickerViewController, didSelectTab tab: Tab) - func tabPicker(_ picker: TabPickerViewController, willCloseTab tab: Tab) + func tabPicker(_ picker: TabPickerViewController, createNewTabWithURL: URL?) + func tabPicker(_ picker: TabPickerViewController, didSelectTabIdentifier tab: UUID) + func tabPicker(_ picker: TabPickerViewController, closeTabWithIdentifier tab: UUID) + func tabPicker(_ picker: TabPickerViewController, tabInfoForIdentifier: UUID) -> TabInfo } class TabPickerViewController: UIViewController, UICollectionViewDelegate { - let tabController: TabController! - var selectedTab: Tab? - - weak var delegate: TabPickerViewControllerDelegate? - typealias TabID = UUID - private var selectedTabsForEditing: Set = [] - private var collectionView: UICollectionView? - private var dataSource: UICollectionViewDiffableDataSource? + public var selectedTabIdentifier: UUID? + public var tabIdentifiers: [ UUID ] = [] { + didSet { + var snapshot = dataSource.snapshot() + snapshot.deleteAllItems() + snapshot.appendSections([ 0 ]) + snapshot.appendItems(tabIdentifiers) + dataSource.apply(snapshot) + } + } + + weak var delegate: TabPickerViewControllerDelegate? + public var tabObserver: AnyCancellable? + private var selectedTabIdentifiersForEditing: Set = [] + + private var listConfiguration = UICollectionLayoutListConfiguration(appearance: .insetGrouped) + private lazy var listLayout = UICollectionViewCompositionalLayout.list(using: listConfiguration) + private lazy var collectionView = UICollectionView(frame: .zero, collectionViewLayout: listLayout).conf { collectionView in + collectionView.allowsMultipleSelectionDuringEditing = true + collectionView.backgroundColor = .systemGroupedBackground + collectionView.delegate = self + } + + private lazy var cellRegistry = UICollectionView.CellRegistration { [unowned self] (listCell, indexPath, item) in + var config = listCell.defaultContentConfiguration() + + if let tab = delegate?.tabPicker(self, tabInfoForIdentifier: item) { + if let title = tab.title, title.count > 0 { + config.text = title + config.secondaryText = tab.url?.absoluteString + } else if let url = tab.url { + config.text = url.absoluteString + config.secondaryText = url.absoluteString + } else { + config.text = "New Tab" + } + + config.textProperties.numberOfLines = 1 + config.secondaryTextProperties.numberOfLines = 1 + + if let image = tab.favicon { + config.image = image + } else { + config.image = UIImage(systemName: "safari") + } + + config.imageProperties.maximumSize = CGSize(width: 21.0, height: 21.0) + config.imageProperties.cornerRadius = 3.0 + + if let selectedTabIdentifier, selectedTabIdentifier == item { + listCell.accessories = [ .checkmark() ] + } else { + listCell.accessories = [] + } + } + + listCell.contentConfiguration = config + } + + private lazy var dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) + { [unowned self] (collectionView, indexPath, item) -> UICollectionViewCell? in + return collectionView.dequeueConfiguredReusableCell(using: cellRegistry, for: indexPath, item: item) + } override var traitCollection: UITraitCollection { get { return super.traitCollection.alwaysPadLike() } @@ -32,8 +90,7 @@ class TabPickerViewController: UIViewController, UICollectionViewDelegate private lazy var newTabButton: UIBarButtonItem = { UIBarButtonItem(systemItem: .add, primaryAction: UIAction(handler: { [unowned self] _ in - let newTab = self.tabController.createNewTab(url: nil) - self.delegate?.tabPicker(self, didSelectTab: newTab) + self.delegate?.tabPicker(self, createNewTabWithURL: nil) }), menu: nil) }() @@ -43,10 +100,8 @@ class TabPickerViewController: UIViewController, UICollectionViewDelegate }), menu: nil) }() - init(tabController: TabController) { - self.tabController = tabController + init() { super.init(nibName: nil, bundle: nil) - self.title = "Tabs" } @@ -55,83 +110,21 @@ class TabPickerViewController: UIViewController, UICollectionViewDelegate } override func loadView() { - var listConfig = UICollectionLayoutListConfiguration(appearance: .insetGrouped) - listConfig.trailingSwipeActionsConfigurationProvider = { [unowned self] indexPath in - if self.dataSource?.snapshot().numberOfItems ?? 0 <= 1 { - return nil - } - + // Load this lazy var now. + _ = cellRegistry.self + + listConfiguration.trailingSwipeActionsConfigurationProvider = { [unowned self] indexPath in return UISwipeActionsConfiguration(actions: [ UIContextualAction(style: .destructive, title: "Close", handler: { [unowned self] (action, view, completionHandler) in - 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) - } + if let item = dataSource.itemIdentifier(for: indexPath) { + var snapshot = dataSource.snapshot() + delegate?.tabPicker(self, closeTabWithIdentifier: item) + + snapshot.deleteItems([ item ]) + dataSource.apply(snapshot, animatingDifferences: true) } })]) } - - let listLayout = UICollectionViewCompositionalLayout.list(using: listConfig) - let collectionView = UICollectionView(frame: .zero, collectionViewLayout: listLayout) - collectionView.allowsMultipleSelectionDuringEditing = true - collectionView.backgroundColor = .systemGroupedBackground - - let registry = UICollectionView.CellRegistration { [unowned self] (listCell, indexPath, item) in - var config = listCell.defaultContentConfiguration() - - if let tab = self.tabController.tab(forIdentifier: item) { - if let title = tab.title, title.count > 0 { - config.text = title - config.secondaryText = tab.url?.absoluteString - } else if let url = tab.url { - config.text = url.absoluteString - config.secondaryText = url.absoluteString - } else { - config.text = "New Tab" - } - - config.textProperties.numberOfLines = 1 - config.secondaryTextProperties.numberOfLines = 1 - - if let image = tab.favicon { - config.image = image - } else { - config.image = UIImage(systemName: "safari") - } - - config.imageProperties.maximumSize = CGSize(width: 21.0, height: 21.0) - config.imageProperties.cornerRadius = 3.0 - - if self.selectedTab == tab { - 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 configureNavigationButtons(forEditing: isEditing) @@ -141,7 +134,7 @@ class TabPickerViewController: UIViewController, UICollectionViewDelegate if !forEditing { navigationItem.rightBarButtonItem = newTabButton } else { - deleteTabButton.isEnabled = collectionView?.indexPathsForSelectedItems?.count ?? 0 > 0 + deleteTabButton.isEnabled = collectionView.indexPathsForSelectedItems?.count ?? 0 > 0 navigationItem.rightBarButtonItem = deleteTabButton } @@ -150,43 +143,37 @@ class TabPickerViewController: UIViewController, UICollectionViewDelegate override func setEditing(_ editing: Bool, animated: Bool) { super.setEditing(editing, animated: animated) - - if let collectionView = collectionView { - collectionView.isEditing = editing - } + collectionView.isEditing = editing configureNavigationButtons(forEditing: editing) } private func deleteSelectedTabs() { - guard let dataSource = dataSource else { return } - var snapshot = dataSource.snapshot() - for tab in selectedTabsForEditing { - snapshot.deleteItems([ tab.identifier ]) - self.delegate?.tabPicker(self, willCloseTab: tab) - self.tabController.closeTab(tab) + for tab in selectedTabIdentifiersForEditing { + snapshot.deleteItems([ tab ]) + delegate?.tabPicker(self, closeTabWithIdentifier: tab) } dataSource.apply(snapshot, animatingDifferences: true) } func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - let tab = tabController.tabs[indexPath.row] + guard let tab = dataSource.itemIdentifier(for: indexPath) else { return } if !isEditing { - delegate?.tabPicker(self, didSelectTab: tab) + delegate?.tabPicker(self, didSelectTabIdentifier: tab) } else { deleteTabButton.isEnabled = collectionView.indexPathsForSelectedItems?.count ?? 0 > 0 - selectedTabsForEditing.update(with: tab) + selectedTabIdentifiersForEditing.update(with: tab) } } func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) { + guard let tabIdentifier = dataSource.itemIdentifier(for: indexPath) else { return } + if isEditing { - let tab = tabController.tabs[indexPath.row] - selectedTabsForEditing.remove(tab) - + selectedTabIdentifiersForEditing.remove(tabIdentifier) deleteTabButton.isEnabled = collectionView.indexPathsForSelectedItems?.count ?? 0 > 0 } } diff --git a/App/Utilities/UIView+Utils.swift b/App/Utilities/UIView+Utils.swift new file mode 100644 index 0000000..5a96570 --- /dev/null +++ b/App/Utilities/UIView+Utils.swift @@ -0,0 +1,20 @@ +// +// UIView+Utils.swift +// App +// +// Created by James Magahern on 8/5/22. +// + +import UIKit + +protocol Conf { } + +extension Conf { + @discardableResult + func conf(_ block: (Self) -> Void) -> Self { + block(self) + return self + } +} + +extension UIView: Conf {} diff --git a/SBrowser.xcodeproj/project.pbxproj b/SBrowser.xcodeproj/project.pbxproj index aa4721c..e78cc14 100644 --- a/SBrowser.xcodeproj/project.pbxproj +++ b/SBrowser.xcodeproj/project.pbxproj @@ -57,6 +57,8 @@ 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 */; }; + CD936A3B289DB3380093A1AC /* TabInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD936A3A289DB3380093A1AC /* TabInfo.swift */; }; + CD936A3D289DB88B0093A1AC /* UIView+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD936A3C289DB88B0093A1AC /* UIView+Utils.swift */; }; CD97CF9225D5BE6F00288FEE /* NavigationControlsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD97CF9125D5BE6F00288FEE /* NavigationControlsView.swift */; }; CD9B88C2272201E900DAAB7E /* SBRScriptPolicy.m in Sources */ = {isa = PBXBuildFile; fileRef = CD361CF5271A3718006E9CA5 /* SBRScriptPolicy.m */; }; CDAD9CE8263A2DF200FF7199 /* DocumentControlsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDAD9CE7263A2DF200FF7199 /* DocumentControlsView.swift */; }; @@ -162,6 +164,8 @@ 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 = ""; }; + CD936A3A289DB3380093A1AC /* TabInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabInfo.swift; sourceTree = ""; }; + CD936A3C289DB88B0093A1AC /* UIView+Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Utils.swift"; sourceTree = ""; }; CD97CF9125D5BE6F00288FEE /* NavigationControlsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationControlsView.swift; sourceTree = ""; }; CDAD9CE7263A2DF200FF7199 /* DocumentControlsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentControlsView.swift; sourceTree = ""; }; CDAD9CE9263A318F00FF7199 /* ShareableURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareableURL.swift; sourceTree = ""; }; @@ -227,6 +231,7 @@ isa = PBXGroup; children = ( 1A14FC2724D26749009B3F83 /* Tab.swift */, + CD936A3A289DB3380093A1AC /* TabInfo.swift */, CD01D5A4254A10BB00189CDC /* TabBarView.swift */, CD01D5AA254A206D00189CDC /* TabBarViewController.swift */, 1AB88EFC24D3BA560006F850 /* TabController.swift */, @@ -375,6 +380,7 @@ 1ADFF4C624CA6DEB006DC7AE /* UIEdgeInsets+Layout.swift */, 1AB88F0524D4D3A90006F850 /* UIGestureRecognizer+Actions.swift */, CDEDD8A925D62ADB00862605 /* UITraitCollection+MacLike.swift */, + CD936A3C289DB88B0093A1AC /* UIView+Utils.swift */, ); path = Utilities; sourceTree = ""; @@ -585,6 +591,7 @@ CD470C4225DE056600AFBE0E /* BrowserViewController+WebKitDelegate.swift in Sources */, CDEDD8AA25D62ADB00862605 /* UITraitCollection+MacLike.swift in Sources */, CD7A8919251989C90075991E /* UIKeyCommand+ConvInit.swift in Sources */, + CD936A3B289DB3380093A1AC /* TabInfo.swift in Sources */, 1ADFF4C724CA6DEB006DC7AE /* UIEdgeInsets+Layout.swift in Sources */, 1ADFF4AE24C8ED32006DC7AE /* ResourcePolicyManager.swift in Sources */, 1ADFF47424C7DE9C006DC7AE /* BrowserViewController.swift in Sources */, @@ -592,6 +599,7 @@ CDCE2668251AAA9A007FE92A /* FontSizeAdjustView.swift in Sources */, CD01D5A5254A10BB00189CDC /* TabBarView.swift in Sources */, 1A03810D24E71CA700826501 /* ToolbarView.swift in Sources */, + CD936A3D289DB88B0093A1AC /* UIView+Utils.swift in Sources */, CD470C4425DE070400AFBE0E /* BrowserViewController+Keyboard.swift in Sources */, CDD0522425F8055700DD1771 /* SearchProvider.swift in Sources */, CD853BD424E77BF900D2BDCC /* HistoryItem.swift in Sources */,