diff --git a/Zotero/Scenes/Detail/Items/Views/ItemsViewController.swift b/Zotero/Scenes/Detail/Items/Views/ItemsViewController.swift index 87812f0b5..ea328b56f 100644 --- a/Zotero/Scenes/Detail/Items/Views/ItemsViewController.swift +++ b/Zotero/Scenes/Detail/Items/Views/ItemsViewController.swift @@ -14,11 +14,6 @@ import RxSwift import WebKit final class ItemsViewController: UIViewController { - private enum SearchBarPosition { - case titleView - case navigationItem - } - private enum RightBarButtonItem: Int { case select case done @@ -49,8 +44,6 @@ final class ItemsViewController: UIViewController { private var tableViewHandler: ItemsTableViewHandler! private var toolbarController: ItemsToolbarController! private var resultsToken: NotificationToken? - private weak var searchBarContainer: SearchBarContainer? - private var searchBarNeedsReset = false private weak var webView: WKWebView? weak var tagFilterDelegate: ItemsTagFilterDelegate? @@ -88,17 +81,14 @@ final class ItemsViewController: UIViewController { }) self.setupRightBarButtonItems(for: self.viewModel.state) self.setupTitle() - // Use `navigationController.view.frame` if available, because the navigation controller is already initialized and layed out, so the view - // size is already calculated properly. - self.setupSearchBar(for: (self.navigationController?.view.frame.size ?? self.view.frame.size)) + self.setupSearchBar() self.setupPullToRefresh() self.setupFileObservers() - self.setupAppStateObserver() self.setupOverlay() self.startObservingSyncProgress() if let term = self.viewModel.state.searchTerm, !term.isEmpty { - self.searchBarContainer?.searchBar.text = term + navigationItem.searchController?.searchBar.text = term } if let results = self.viewModel.state.results { self.startObserving(results: results) @@ -131,18 +121,6 @@ final class ItemsViewController: UIViewController { super.viewDidAppear(animated) } - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - // Workaround for broken `titleView` animation, check `SearchBarContainer` for more info. - self.searchBarContainer?.freezeWidth() - } - - override func viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) - // Workaround for broken `titleView` animation, check `SearchBarContainer` for more info. - self.searchBarContainer?.unfreezeWidth() - } - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) @@ -152,21 +130,13 @@ final class ItemsViewController: UIViewController { self.toolbarController.reloadToolbarItems(for: self.viewModel.state) } } - - override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { - super.viewWillTransition(to: size, with: coordinator) - - coordinator.animate { _ in - self.setupSearchBar(for: size) - } - } override func pressesBegan(_ presses: Set, with event: UIPressesEvent?) { guard let key = presses.first?.key, key.characters == "f", key.modifierFlags.contains(.command) else { super.pressesBegan(presses, with: event) return } - self.searchBarContainer?.searchBar.becomeFirstResponder() + navigationItem.searchController?.searchBar.becomeFirstResponder() } deinit { @@ -250,12 +220,6 @@ final class ItemsViewController: UIViewController { } } - func setSearchBarNeedsReset() { - // Only reset search bar if it's in the title view. It disappears only from the navigation item title view. - guard self.navigationItem.titleView != nil else { return } - self.searchBarNeedsReset = true - } - // MARK: - Actions private func handle(action: ItemsTableViewHandler.TapAction) { @@ -432,23 +396,9 @@ final class ItemsViewController: UIViewController { } } - // This is a workaround for setting a `navigationItem.searchController` after appearance of this controller on iPad. - // If `searchController` is set after controller appears on screen, it can create visual artifacts (navigation bar shows second row with - // nothing in it) or freeze the `tableView` scroll (user has to manually pop back to previous screen and reopen this controller to - // be able to scroll). The `navigationItem` is fixed when there is a transition to another `UIViewController`. So we fake a transition - // by pushing empty `UIViewController` and popping back to this one without animation, which fixes everything. - private func resetSearchBar() { - let controller = UIViewController() - self.navigationController?.pushViewController(controller, animated: false) - self.navigationController?.popViewController(animated: false) - } - private func resetActiveSearch() { - if let searchBar = self.searchBarContainer?.searchBar { - searchBar.resignFirstResponder() - } else if let controller = self.navigationItem.searchController { - controller.searchBar.resignFirstResponder() - } + guard let searchBar = navigationItem.searchController?.searchBar else { return } + searchBar.resignFirstResponder() } private func startObserving(results: Results) { @@ -516,19 +466,6 @@ final class ItemsViewController: UIViewController { // MARK: - Setups - private func setupAppStateObserver() { - NotificationCenter.default - .rx - .notification(.willEnterForeground) - .observe(on: MainScheduler.instance) - .subscribe(onNext: { [weak self] _ in - guard let self = self, self.searchBarNeedsReset else { return } - self.resetSearchBar() - self.searchBarNeedsReset = false - }) - .disposed(by: self.disposeBag) - } - private func setupFileObservers() { NotificationCenter.default .rx @@ -679,71 +616,20 @@ final class ItemsViewController: UIViewController { return item } - /// Setup `searchBar` for current window size. If there is enough space for the `searchBar` in `titleView`, it's added there, otherwise it's added - /// to `navigationItem`, where it appears under `navigationBar`. - /// - parameter windowSize: Current window size. - /// - returns: New search bar position - @discardableResult - private func setupSearchBar(for windowSize: CGSize) -> SearchBarPosition { - // Detect current position of search bar - let current: SearchBarPosition? = navigationItem.searchController != nil ? .navigationItem : (navigationItem.titleView != nil ? .titleView : nil) - // The search bar can change position based on current window size. If the window is too narrow, the search bar appears in - // navigationItem, otherwise it can appear in titleView. - let new: SearchBarPosition = traitCollection.horizontalSizeClass == .compact ? .navigationItem : .titleView - - // Only change search bar if the position changes. - guard current != new else { return new } - - removeSearchBar() - setupSearchBar(in: new) - - return new - - /// Removes `searchBar` from all positions. - func removeSearchBar() { - if navigationItem.searchController != nil { - navigationItem.searchController = nil - } - if navigationItem.titleView != nil { - navigationItem.titleView = nil - } - searchBarContainer = nil - } - - /// Setup `searchBar` in given position. - /// - parameter position: Position of `searchBar`. - func setupSearchBar(in position: SearchBarPosition) { - switch position { - case .titleView: - let searchBar = UISearchBar() - setup(searchBar: searchBar) - // Workaround for broken `titleView` animation, check `SearchBarContainer` for more info. - let container = SearchBarContainer(searchBar: searchBar) - navigationItem.titleView = container - searchBarContainer = container - - case .navigationItem: - let controller = UISearchController(searchResultsController: nil) - setup(searchBar: controller.searchBar) - controller.obscuresBackgroundDuringPresentation = false - navigationItem.hidesSearchBarWhenScrolling = false - navigationItem.searchController = controller - } - - /// Setup `searchBar`, start observing text changes. - /// - parameter searchBar: `searchBar` to setup and observe. - func setup(searchBar: UISearchBar) { - searchBar.placeholder = L10n.Items.searchTitle - searchBar.rx - .text.observe(on: MainScheduler.instance) - .skip(1) - .debounce(.milliseconds(150), scheduler: MainScheduler.instance) - .subscribe(onNext: { [weak self] text in - self?.viewModel.process(action: .search(text ?? "")) - }) - .disposed(by: disposeBag) - } - } + private func setupSearchBar() { + let controller = UISearchController(searchResultsController: nil) + controller.searchBar.placeholder = L10n.Items.searchTitle + controller.searchBar.rx + .text.observe(on: MainScheduler.instance) + .skip(1) + .debounce(.milliseconds(150), scheduler: MainScheduler.instance) + .subscribe(onNext: { [weak self] text in + self?.viewModel.process(action: .search(text ?? "")) + }) + .disposed(by: disposeBag) + controller.obscuresBackgroundDuringPresentation = false + navigationItem.hidesSearchBarWhenScrolling = false + navigationItem.searchController = controller } private func setupPullToRefresh() { @@ -794,68 +680,6 @@ extension ItemsViewController: ItemsToolbarControllerDelegate { } } -/// -/// This is a container for `UISearchBar` to fix broken UIKit `titleView` animation in navigation bar. -/// The `titleView` is assigned an expanding view (`UISearchBar`), so the `titleView` expands to full width on animation to different screen. -/// For example, if the new screen has fewer `rightBarButtonItems`, the `titleView` width expands and the animation looks as if the search bar is -/// moving to the right, even though the screen is animating out to the left. -/// -/// To fix this, the `titleView` needs to have a set width. I didn't want to use hardcoded values and calculate the available `titleView` width -/// manually, so I created this view. -/// -/// The point is that this view is expandable (`intrinsicContentSize` width set to `.greatestFiniteMagnitude`). The child `searchBar` has trailing -/// constraint less or equal than trailing constraint of parent `SearchBarContainer`. But the width constraint of search bar is set to -/// `.greatestFiniteMagnitude` with low priority. So by default the search bar expands as much as possible, but is limited by parent width. -/// Then, when parent controller is leaving screen on `viewWillDisappear` it calls `freezeWidth()` to freeze the search bar width by setting width -/// constraint to current width of search bar. When the animation finishes the parent controller has to call `unfreezeWidth()` to set the width back -/// to `.greatestFiniteMagnitude`, so that it stretches to appropriate size when needed (for example when the device rotates). -/// -private final class SearchBarContainer: UIView { - unowned let searchBar: UISearchBar - private var widthConstraint: NSLayoutConstraint! - private var maxSize: CGFloat { - max(UIScreen.main.bounds.size.width, UIScreen.main.bounds.size.height) - } - - init(searchBar: UISearchBar) { - searchBar.translatesAutoresizingMaskIntoConstraints = false - self.searchBar = searchBar - - super.init(frame: CGRect()) - - self.addSubview(searchBar) - - NSLayoutConstraint.activate([ - searchBar.topAnchor.constraint(equalTo: self.topAnchor), - searchBar.bottomAnchor.constraint(equalTo: self.bottomAnchor), - searchBar.leadingAnchor.constraint(equalTo: self.leadingAnchor), - searchBar.trailingAnchor.constraint(lessThanOrEqualTo: self.trailingAnchor) - ]) - - self.widthConstraint = self.searchBar.widthAnchor.constraint(equalToConstant: maxSize) - self.widthConstraint.priority = .defaultLow - self.widthConstraint.isActive = true - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override var intrinsicContentSize: CGSize { - // Changed from .greatestFiniteValue to this because of error "This NSLayoutConstraint is being configured with a constant that exceeds - // internal limits." This works fine as well and the debugger doesn't show the error anymore. - return CGSize(width: maxSize, height: self.searchBar.bounds.height) - } - - func freezeWidth() { - self.widthConstraint.constant = self.searchBar.frame.width - } - - func unfreezeWidth() { - self.widthConstraint.constant = maxSize - } -} - extension ItemsViewController: TagFilterDelegate { var currentLibrary: Library { return self.viewModel.state.library diff --git a/Zotero/Scenes/Master/MasterContainerViewController.swift b/Zotero/Scenes/Master/MasterContainerViewController.swift index 1c2c563ee..280a4a17c 100644 --- a/Zotero/Scenes/Master/MasterContainerViewController.swift +++ b/Zotero/Scenes/Master/MasterContainerViewController.swift @@ -330,11 +330,6 @@ final class MasterContainerViewController: UINavigationController { override func collapseSecondaryViewController(_ secondaryViewController: UIViewController, for splitViewController: UISplitViewController) { setBottomSheet(hidden: true) super.collapseSecondaryViewController(secondaryViewController, for: splitViewController) - // The search bar is hidden when the app goes to background for unknown reason. This is a workaround to reset it if needed when - // the app returns to active state. - if let controller = (secondaryViewController as? UINavigationController)?.topViewController as? ItemsViewController { - controller.setSearchBarNeedsReset() - } } override func separateSecondaryViewController(for splitViewController: UISplitViewController) -> UIViewController? {