Skip to content

Commit

Permalink
Remove old style of search bar, replace with iOS default style
Browse files Browse the repository at this point in the history
  • Loading branch information
michalrentka committed Sep 20, 2023
1 parent d878e2a commit 8eb6371
Show file tree
Hide file tree
Showing 2 changed files with 19 additions and 200 deletions.
214 changes: 19 additions & 195 deletions Zotero/Scenes/Detail/Items/Views/ItemsViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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?

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)

Expand All @@ -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<UIPress>, 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 {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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<RItem>) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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
Expand Down
5 changes: 0 additions & 5 deletions Zotero/Scenes/Master/MasterContainerViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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? {
Expand Down

0 comments on commit 8eb6371

Please sign in to comment.