Подтвердить что ты не робот

Сломанная анимация UISearchBar, встроенная в NavigationItem

У меня возникла проблема с новым способом добавления панели поиска к элементу навигации.

Как вы можете видеть на картинке ниже, есть два UIViewController один за другим, и оба имеют панель поиска. Проблема заключается в анимации, которая выглядит ужасно, когда строка поиска видна на первом контроллере представления, но не на втором. Область, занимаемая строкой поиска, остается на экране и внезапно исчезает.

Demo

Код очень простой (никаких других изменений в проекте не было):

(Я пишу в основном на С#, поэтому в этом коде могут быть ошибки.)

ViewController.swift:

import UIKit

class ViewController: UITableViewController, UISearchResultsUpdating {

override func loadView() {
    super.loadView()

    definesPresentationContext = true;

    navigationController?.navigationBar.prefersLargeTitles = true;
    navigationItem.largeTitleDisplayMode = .automatic;
    navigationItem.title = "VC"

    tableView.insetsContentViewsToSafeArea = true;
    tableView.dataSource = self;

    refreshControl = UIRefreshControl();
    refreshControl?.addTarget(self, action: #selector(ViewController.handleRefresh(_:)), for: UIControlEvents.valueChanged)
    tableView.refreshControl = refreshControl;

    let stvc = UITableViewController();
    stvc.tableView.dataSource = self;

    let sc = UISearchController(searchResultsController: stvc);
    sc.searchResultsUpdater = self;
    navigationItem.searchController = sc;
}

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    var cell = tableView.dequeueReusableCell(withIdentifier: "cell1");
    if (cell == nil) {
        cell = UITableViewCell(style: .default, reuseIdentifier: "cell1");
    }
    cell?.textLabel?.text = "cell " + String(indexPath.row);
    return cell!;
}

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return 20;
}

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    let vc = ViewController();
    navigationController?.pushViewController(vc, animated: true);
}

@objc func handleRefresh(_ refreshControl: UIRefreshControl) {
    DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2), execute: {
        refreshControl.endRefreshing();
    })
}

func updateSearchResults(for searchController: UISearchController) {
}
}

AppDelegate.swift:

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

var window: UIWindow?

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {

    window = UIWindow(frame: UIScreen.main.bounds);
    window?.rootViewController = UINavigationController(rootViewController: ViewController());
    window?.makeKeyAndVisible();

    UINavigationBar.appearance().barTintColor = UIColor.red;

    return true
}
}

Идеи?

4b9b3361

Ответ 1

Похоже, Apple все еще нуждается в том, чтобы сгладить использование UISearchBar в новом стиле большого заголовка. Если UIViewController, на который вы нажимаете, не имеет установленного navigationItem.searchController, анимация работает нормально. При навигации между двумя экземплярами UIViewController, у которых есть набор searchController, вы получаете описанную проблему, где скачка высоты навигационной панели.

Вы можете решить (обойти) проблему, создав UISearchController каждый раз, когда вызывается viewDidAppear (вместо создания в loadView) и устанавливая navigationItem.searchController на nil на viewDidDisappear.

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)

    DispatchQueue.main.async {
        let stvc = UITableViewController()
        stvc.tableView.dataSource = self

        let sc = UISearchController(searchResultsController: stvc)
        sc.searchResultsUpdater = self
        self.navigationItem.searchController = sc
    }
}

override func viewDidDisappear(_ animated: Bool) {
    super.viewDidDisappear(animated)

    self.navigationItem.searchController = nil
}

Причиной асинхронной отправки является то, что при установке navigationItem.searchController inline в методе viewDidAppear возникает исключение:

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Only one palette with a top boundary edge can be active outside of a transition. Current active palette is <_UINavigationControllerManagedSearchPalette: 0x7fad67117e80; frame = (0 116; 414 0); layer = <CALayer: 0x60400002c8e0>>'

Я знаю, что это только работа, но, надеюсь, это пока поможет вам, пока Apple не решит проблему с помощью навигации между двумя контроллерами представлений, которые имеют UISearchController, установленные на их navigationItem.

Ответ 2

Принятый ответ действительно решает проблему для некоторых ситуаций, но я испытывал его, приводя к полному удалению navigationItem элемента в контроллере push-представления, если была активна первая строка поиска.

Я придумал другой обходной путь, похожий на ответ от stu, но не требующий вмешательства с ограничениями. Подход заключается в том, чтобы определить в момент перехода, видна ли строка поиска. Если это так, мы указываем целевому контроллеру представления сделать его строку поиска видимой из загрузки. Это означает, что анимация элемента навигации ведет себя правильно:

Search bar correctly animating

Предполагая, что два контроллера представления называются UIViewController1 и UIViewController2, где 1 нажимает 2, код выглядит следующим образом:

class ViewController1: UITableViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let searchController = UISearchController(searchResultsController: nil)
        searchController.obscuresBackgroundDuringPresentation = false
        navigationItem.searchController = searchController

        definesPresentationContext = true
    }

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if let viewController2 = segue.destination as? ViewController2, let searchController = navigationItem.searchController {

            // If the search bar is visible (but not active, which would make it visible but at the top of the view)
            // in this view controller as we are preparing to segue, instruct the destination view controller that its
            // search bar should be visible from load.
            viewController2.forceSearchBarVisibleOnLoad = !searchController.isActive && searchController.searchBar.frame.height > 0
        }
    }
}
class ViewController2: UITableViewController {

    var forceSearchBarVisibleOnLoad = false

    override func viewDidLoad() {
        super.viewDidLoad()

        let searchController = UISearchController(searchResultsController: nil)
        searchController.obscuresBackgroundDuringPresentation = false
        navigationItem.searchController = searchController

        // If on load we want to force the search bar to be visible, we make it so that it is always visible to start with
        if forceSearchBarVisibleOnLoad {
            navigationItem.hidesSearchBarWhenScrolling = false
        }
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        // When the view has appeared, we switch back the default behaviour of the search bar being hideable.
        // The search bar will already be visible at this point, thus achieving what we aimed to do (have it
        // visible during the animation).
        navigationItem.hidesSearchBarWhenScrolling = true
    }
}

Ответ 3

Мое решение для этой проблемы состоит в том, чтобы обновить ограничение, которое сохраняет UISearchBar видимым, когда UIViewController. Я не смог использовать решение silic_valley, так как даже с асинхронной отправкой я получал описанную им аварию. По общему признанию это довольно грязное решение, но Apple не сделала это легким.

Приведенный ниже код предполагает у вас есть свойство, содержащее UISearchController экземпляр в пределах вашего UIViewController подкласса под названием searchController

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)

    if
        animated
            && !searchController.isActive
            && !searchController.isEditing
            && navigationController.map({$0.viewControllers.last != self}) ?? false,
        let searchBarSuperview = searchController.searchBar.superview,
        let searchBarHeightConstraint = searchBarSuperview.constraints.first(where: {
            $0.firstAttribute == .height
                && $0.secondItem == nil
                && $0.secondAttribute == .notAnAttribute
                && $0.constant > 0
        }) {

        UIView.performWithoutAnimation {
            searchBarHeightConstraint.constant = 0
            searchBarSuperview.superview?.layoutIfNeeded()
        }
    }
}

Вы можете удалить performWithoutAnimation и layoutIfNeeded, и он все равно будет анимирован; однако я обнаружил, что анимация никогда не запускалась с первого раза, и в любом случае она выглядит не очень хорошо.

Я надеюсь, что Apple исправит это в более позднем выпуске iOS, текущий выпуск - 12.1.4 на момент написания.

Ответ 4

В VC1:

override func viewDidLoad() {
   if #available(iOS 11.0, *) {
        navigationItem.hidesSearchBarWhenScrolling = false
        navigationItem.searchController = searchController
    }
}
override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    if #available(iOS 11.0, *) {
        navigationItem.hidesSearchBarWhenScrolling = true
    }
}

В VC2 используйте тот же код.

Это решит вашу проблему более четко, чем установление serchController на nil

Ответ 5

Эта проблема была исправлена в iOS 13 beta 1. Проверьте свои обходные пути перед обновлением приложения на наличие новой версии.

Ответ 6

Я добавил этот код в viewDidLoad() и он работал, когда я переместился в ч/б вкладок

searchController.dimsBackgroundDuringPresentation