ChatListController.swift 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617
  1. import UIKit
  2. import DcCore
  3. class ChatListController: UITableViewController {
  4. var viewModel: ChatListViewModel?
  5. let dcContext: DcContext
  6. var isArchive: Bool
  7. private let chatCellReuseIdentifier = "chat_cell"
  8. private let deadDropCellReuseIdentifier = "deaddrop_cell"
  9. private let contactCellReuseIdentifier = "contact_cell"
  10. private var msgChangedObserver: NSObjectProtocol?
  11. private var msgsNoticedObserver: NSObjectProtocol?
  12. private var incomingMsgObserver: NSObjectProtocol?
  13. private var chatModifiedObserver: NSObjectProtocol?
  14. private var contactsChangedObserver: NSObjectProtocol?
  15. private var connectivityChangedObserver: NSObjectProtocol?
  16. private var msgChangedSearchResultObserver: NSObjectProtocol?
  17. private weak var timer: Timer?
  18. private lazy var titleView: UILabel = {
  19. let view = UILabel()
  20. let navTapGesture = UITapGestureRecognizer(target: self, action: #selector(onNavigationTitleTapped))
  21. view.addGestureRecognizer(navTapGesture)
  22. view.isUserInteractionEnabled = true
  23. view.font = UIFont.systemFont(ofSize: 17, weight: .semibold)
  24. return view
  25. }()
  26. private lazy var searchController: UISearchController = {
  27. let searchController = UISearchController(searchResultsController: nil)
  28. searchController.obscuresBackgroundDuringPresentation = false
  29. searchController.searchBar.placeholder = String.localized("search")
  30. return searchController
  31. }()
  32. private lazy var archiveCell: ActionCell = {
  33. let actionCell = ActionCell()
  34. return actionCell
  35. }()
  36. private lazy var newButton: UIBarButtonItem = {
  37. let button = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.compose, target: self, action: #selector(didPressNewChat))
  38. button.tintColor = DcColors.primary
  39. return button
  40. }()
  41. private lazy var cancelButton: UIBarButtonItem = {
  42. let button = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelButtonPressed))
  43. return button
  44. }()
  45. private lazy var emptyStateLabel: EmptyStateLabel = {
  46. let label = EmptyStateLabel()
  47. label.isHidden = false
  48. return label
  49. }()
  50. init(dcContext: DcContext, isArchive: Bool) {
  51. self.dcContext = dcContext
  52. self.isArchive = isArchive
  53. super.init(style: .grouped)
  54. DispatchQueue.global(qos: .userInteractive).async { [weak self] in
  55. guard let self = self else { return }
  56. self.viewModel = ChatListViewModel(dcContext: self.dcContext, isArchive: isArchive)
  57. self.viewModel?.onChatListUpdate = self.handleChatListUpdate
  58. DispatchQueue.main.async { [weak self] in
  59. guard let self = self else { return }
  60. if !isArchive {
  61. self.navigationItem.searchController = self.searchController
  62. self.searchController.searchResultsUpdater = self.viewModel
  63. self.searchController.searchBar.delegate = self
  64. }
  65. self.tableView.reloadData()
  66. }
  67. }
  68. }
  69. required init?(coder _: NSCoder) {
  70. fatalError("init(coder:) has not been implemented")
  71. }
  72. // MARK: - lifecycle
  73. override func viewDidLoad() {
  74. super.viewDidLoad()
  75. if isArchive {
  76. navigationItem.rightBarButtonItem = newButton
  77. }
  78. configureTableView()
  79. setupSubviews()
  80. // update messages - for new messages, do not reuse or modify strings but create new ones.
  81. // it is not needed to keep all past update messages, however, when deleted, also the strings should be deleted.
  82. let msg = dcContext.newMessage(viewType: DC_MSG_TEXT)
  83. msg.text = String.localized("update_1_28_android") + "\n\n" + String.localized("update_1_28_ios_extra_line")
  84. dcContext.addDeviceMessage(label: "update_1_28a_ios", msg: msg)
  85. handleEmptyStateLabel()
  86. }
  87. override func willMove(toParent parent: UIViewController?) {
  88. super.willMove(toParent: parent)
  89. if parent == nil {
  90. // logger.debug("chat observer: remove")
  91. removeObservers()
  92. } else {
  93. // logger.debug("chat observer: setup")
  94. addObservers()
  95. }
  96. }
  97. override func viewWillAppear(_ animated: Bool) {
  98. super.viewWillAppear(animated)
  99. // create view
  100. navigationItem.titleView = titleView
  101. updateTitle()
  102. if RelayHelper.sharedInstance.isForwarding() {
  103. quitSearch(animated: false)
  104. tableView.scrollToTop()
  105. }
  106. }
  107. override func viewDidAppear(_ animated: Bool) {
  108. super.viewDidAppear(animated)
  109. startTimer()
  110. }
  111. override func viewDidDisappear(_ animated: Bool) {
  112. super.viewDidDisappear(animated)
  113. stopTimer()
  114. }
  115. // MARK: - setup
  116. private func addObservers() {
  117. let nc = NotificationCenter.default
  118. connectivityChangedObserver = nc.addObserver(forName: dcNotificationConnectivityChanged,
  119. object: nil,
  120. queue: nil) { [weak self] _ in
  121. self?.updateTitle()
  122. }
  123. msgChangedSearchResultObserver = nc.addObserver(
  124. forName: dcNotificationChanged,
  125. object: nil,
  126. queue: nil) { [weak self] _ in
  127. guard let self = self else { return }
  128. if let appDelegate = UIApplication.shared.delegate as? AppDelegate,
  129. let viewModel = self.viewModel,
  130. viewModel.searchActive,
  131. appDelegate.appIsInForeground() {
  132. viewModel.updateSearchResults(for: self.searchController)
  133. }
  134. }
  135. msgChangedObserver = nc.addObserver(
  136. forName: dcNotificationChanged,
  137. object: nil,
  138. queue: nil) { [weak self] _ in
  139. self?.refreshInBg()
  140. }
  141. msgsNoticedObserver = nc.addObserver(
  142. forName: dcMsgsNoticed,
  143. object: nil,
  144. queue: nil) { [weak self] _ in
  145. self?.refreshInBg()
  146. }
  147. incomingMsgObserver = nc.addObserver(
  148. forName: dcNotificationIncoming,
  149. object: nil,
  150. queue: nil) { [weak self] _ in
  151. self?.refreshInBg()
  152. }
  153. chatModifiedObserver = nc.addObserver(
  154. forName: dcNotificationChatModified,
  155. object: nil,
  156. queue: nil) { [weak self] _ in
  157. self?.refreshInBg()
  158. }
  159. contactsChangedObserver = nc.addObserver(
  160. forName: dcNotificationContactChanged,
  161. object: nil,
  162. queue: nil) { [weak self] _ in
  163. self?.refreshInBg()
  164. }
  165. nc.addObserver(
  166. self,
  167. selector: #selector(applicationDidBecomeActive(_:)),
  168. name: UIApplication.didBecomeActiveNotification,
  169. object: nil)
  170. nc.addObserver(
  171. self,
  172. selector: #selector(applicationWillResignActive(_:)),
  173. name: UIApplication.willResignActiveNotification,
  174. object: nil)
  175. }
  176. private func removeObservers() {
  177. let nc = NotificationCenter.default
  178. // remove observers with a block
  179. if let msgChangedResultObserver = self.msgChangedSearchResultObserver {
  180. nc.removeObserver(msgChangedResultObserver)
  181. }
  182. if let msgChangedObserver = self.msgChangedObserver {
  183. nc.removeObserver(msgChangedObserver)
  184. }
  185. if let incomingMsgObserver = self.incomingMsgObserver {
  186. nc.removeObserver(incomingMsgObserver)
  187. }
  188. if let msgsNoticedObserver = self.msgsNoticedObserver {
  189. nc.removeObserver(msgsNoticedObserver)
  190. }
  191. if let chatModifiedObserver = self.chatModifiedObserver {
  192. nc.removeObserver(chatModifiedObserver)
  193. }
  194. if let contactsChangedObserver = self.contactsChangedObserver {
  195. nc.removeObserver(contactsChangedObserver)
  196. }
  197. if let connectivityChangedObserver = self.connectivityChangedObserver {
  198. nc.removeObserver(connectivityChangedObserver)
  199. }
  200. // remove non-block observers
  201. NotificationCenter.default.removeObserver(self, name: UIApplication.didBecomeActiveNotification, object: nil)
  202. NotificationCenter.default.removeObserver(self, name: UIApplication.willResignActiveNotification, object: nil)
  203. }
  204. private func setupSubviews() {
  205. emptyStateLabel.addCenteredTo(parentView: view)
  206. navigationItem.backButtonTitle = isArchive ? String.localized("chat_archived_chats_title") : String.localized("pref_chats")
  207. }
  208. @objc
  209. public func onNavigationTitleTapped() {
  210. logger.debug("on navigation title tapped")
  211. let connectivityViewController = ConnectivityViewController(dcContext: dcContext)
  212. navigationController?.pushViewController(connectivityViewController, animated: true)
  213. }
  214. // MARK: - configuration
  215. private func configureTableView() {
  216. tableView.register(ContactCell.self, forCellReuseIdentifier: chatCellReuseIdentifier)
  217. tableView.register(ContactCell.self, forCellReuseIdentifier: deadDropCellReuseIdentifier)
  218. tableView.register(ContactCell.self, forCellReuseIdentifier: contactCellReuseIdentifier)
  219. tableView.rowHeight = ContactCell.cellHeight
  220. }
  221. private var isInitial = true
  222. @objc func applicationDidBecomeActive(_ notification: NSNotification) {
  223. if navigationController?.visibleViewController == self {
  224. if !isInitial {
  225. startTimer()
  226. refreshInBg()
  227. isInitial = false
  228. }
  229. }
  230. }
  231. private var inBgRefresh = false
  232. private var needsAnotherBgRefresh = false
  233. private func refreshInBg() {
  234. if inBgRefresh {
  235. needsAnotherBgRefresh = true
  236. } else {
  237. inBgRefresh = true
  238. DispatchQueue.global(qos: .userInteractive).async { [weak self] in
  239. // do at least one refresh, without inital delay
  240. // (refreshData() calls handleChatListUpdate() on main thread when done)
  241. self?.needsAnotherBgRefresh = false
  242. self?.viewModel?.refreshData()
  243. // do subsequent refreshes with a delay of 500ms
  244. while self?.needsAnotherBgRefresh != false {
  245. usleep(500000)
  246. self?.needsAnotherBgRefresh = false
  247. self?.viewModel?.refreshData()
  248. }
  249. self?.inBgRefresh = false
  250. }
  251. }
  252. }
  253. @objc func applicationWillResignActive(_ notification: NSNotification) {
  254. if navigationController?.visibleViewController == self {
  255. stopTimer()
  256. }
  257. }
  258. // MARK: - actions
  259. @objc func didPressNewChat() {
  260. showNewChatController()
  261. }
  262. @objc func cancelButtonPressed() {
  263. // cancel forwarding
  264. RelayHelper.sharedInstance.cancel()
  265. updateTitle()
  266. refreshInBg()
  267. }
  268. override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
  269. if previousTraitCollection?.preferredContentSizeCategory !=
  270. traitCollection.preferredContentSizeCategory {
  271. tableView.rowHeight = ContactCell.cellHeight
  272. }
  273. }
  274. private func quitSearch(animated: Bool) {
  275. searchController.searchBar.text = nil
  276. self.viewModel?.endSearch()
  277. searchController.dismiss(animated: animated) {
  278. self.tableView.scrollToTop()
  279. }
  280. }
  281. // MARK: - UITableViewDelegate + UITableViewDatasource
  282. override func numberOfSections(in tableView: UITableView) -> Int {
  283. return viewModel?.numberOfSections ?? 0
  284. }
  285. override func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int {
  286. return viewModel?.numberOfRowsIn(section: section) ?? 0
  287. }
  288. override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  289. guard let viewModel = viewModel else {
  290. return UITableViewCell()
  291. }
  292. let cellData = viewModel.cellDataFor(section: indexPath.section, row: indexPath.row)
  293. switch cellData.type {
  294. case .chat(let chatData):
  295. let chatId = chatData.chatId
  296. if chatId == DC_CHAT_ID_ARCHIVED_LINK {
  297. archiveCell.actionTitle = dcContext.getChat(chatId: chatId).name
  298. archiveCell.backgroundColor = DcColors.chatBackgroundColor
  299. return archiveCell
  300. } else if let chatCell = tableView.dequeueReusableCell(withIdentifier: chatCellReuseIdentifier, for: indexPath) as? ContactCell {
  301. // default chatCell
  302. chatCell.updateCell(cellViewModel: cellData)
  303. return chatCell
  304. }
  305. case .contact:
  306. safe_assert(viewModel.searchActive)
  307. if let contactCell = tableView.dequeueReusableCell(withIdentifier: contactCellReuseIdentifier, for: indexPath) as? ContactCell {
  308. contactCell.updateCell(cellViewModel: cellData)
  309. return contactCell
  310. }
  311. case .profile:
  312. safe_fatalError("CellData type profile not allowed")
  313. default:
  314. break
  315. }
  316. safe_fatalError("Could not find/dequeue or recycle UITableViewCell.")
  317. return UITableViewCell()
  318. }
  319. override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
  320. return viewModel?.titleForHeaderIn(section: section)
  321. }
  322. override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  323. guard let viewModel = viewModel else {
  324. tableView.deselectRow(at: indexPath, animated: false)
  325. return
  326. }
  327. let cellData = viewModel.cellDataFor(section: indexPath.section, row: indexPath.row)
  328. switch cellData.type {
  329. case .chat(let chatData):
  330. let chatId = chatData.chatId
  331. if chatId == DC_CHAT_ID_ARCHIVED_LINK {
  332. showArchive(animated: true)
  333. } else {
  334. showChat(chatId: chatId, highlightedMsg: chatData.highlightMsgId)
  335. }
  336. case .contact(let contactData):
  337. let contactId = contactData.contactId
  338. if let chatId = contactData.chatId {
  339. showChat(chatId: chatId)
  340. } else {
  341. self.askToChatWith(contactId: contactId)
  342. }
  343. case .profile:
  344. safe_fatalError("CellData type profile not allowed")
  345. }
  346. tableView.deselectRow(at: indexPath, animated: false)
  347. }
  348. override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
  349. guard let viewModel = viewModel else { return [] }
  350. guard let chatId = viewModel.chatIdFor(section: indexPath.section, row: indexPath.row) else {
  351. return []
  352. }
  353. if chatId==DC_CHAT_ID_ARCHIVED_LINK {
  354. return []
  355. // returning nil may result in a default delete action,
  356. // see https://forums.developer.apple.com/thread/115030
  357. }
  358. let chat = dcContext.getChat(chatId: chatId)
  359. let archived = chat.isArchived
  360. let archiveActionTitle: String = String.localized(archived ? "unarchive" : "archive")
  361. let archiveAction = UITableViewRowAction(style: .destructive, title: archiveActionTitle) { [weak self] _, _ in
  362. self?.viewModel?.archiveChatToggle(chatId: chatId)
  363. }
  364. archiveAction.backgroundColor = UIColor.lightGray
  365. let pinned = chat.visibility==DC_CHAT_VISIBILITY_PINNED
  366. let pinAction = UITableViewRowAction(style: .destructive, title: String.localized(pinned ? "unpin" : "pin")) { [weak self] _, _ in
  367. self?.viewModel?.pinChatToggle(chatId: chat.id)
  368. }
  369. pinAction.backgroundColor = UIColor.systemGreen
  370. let deleteAction = UITableViewRowAction(style: .normal, title: String.localized("delete")) { [weak self] _, _ in
  371. self?.showDeleteChatConfirmationAlert(chatId: chatId)
  372. }
  373. deleteAction.backgroundColor = UIColor.systemRed
  374. return [archiveAction, pinAction, deleteAction]
  375. }
  376. // MARK: updates
  377. private func updateTitle() {
  378. if RelayHelper.sharedInstance.isForwarding() {
  379. titleView.text = String.localized("forward_to")
  380. if isArchive {
  381. navigationItem.setLeftBarButton(cancelButton, animated: true)
  382. }
  383. } else if isArchive {
  384. titleView.text = String.localized("chat_archived_chats_title")
  385. navigationItem.setLeftBarButton(nil, animated: true)
  386. } else {
  387. titleView.text = DcUtils.getConnectivityString(dcContext: dcContext, connectedString: String.localized("pref_chats"))
  388. navigationItem.setLeftBarButton(nil, animated: true)
  389. }
  390. titleView.sizeToFit()
  391. }
  392. func handleChatListUpdate() {
  393. tableView.reloadData()
  394. handleEmptyStateLabel()
  395. }
  396. private func handleEmptyStateLabel() {
  397. if let emptySearchText = viewModel?.emptySearchText {
  398. let text = String.localizedStringWithFormat(
  399. String.localized("search_no_result_for_x"),
  400. emptySearchText
  401. )
  402. emptyStateLabel.text = text
  403. emptyStateLabel.isHidden = false
  404. } else if isArchive && (viewModel?.numberOfRowsIn(section: 0) ?? 0) == 0 {
  405. emptyStateLabel.text = String.localized("archive_empty_hint")
  406. emptyStateLabel.isHidden = false
  407. } else {
  408. emptyStateLabel.text = nil
  409. emptyStateLabel.isHidden = true
  410. }
  411. }
  412. private func startTimer() {
  413. // check if the timer is not yet started
  414. stopTimer()
  415. timer = Timer.scheduledTimer(withTimeInterval: 60, repeats: true) { [weak self] _ in
  416. guard let self = self,
  417. let appDelegate = UIApplication.shared.delegate as? AppDelegate
  418. else { return }
  419. if appDelegate.appIsInForeground() {
  420. self.refreshInBg()
  421. } else {
  422. logger.warning("startTimer() must not be executed in background")
  423. }
  424. }
  425. }
  426. private func stopTimer() {
  427. // check if the timer is not already stopped
  428. if let timer = timer {
  429. timer.invalidate()
  430. }
  431. timer = nil
  432. }
  433. public func handleMailto() {
  434. if let mailtoAddress = RelayHelper.sharedInstance.mailtoAddress {
  435. // FIXME: the line below should work
  436. // var contactId = dcContext.lookupContactIdByAddress(mailtoAddress)
  437. // workaround:
  438. let contacts: [Int] = dcContext.getContacts(flags: DC_GCL_ADD_SELF, queryString: mailtoAddress)
  439. let index = contacts.firstIndex(where: { dcContext.getContact(id: $0).email == mailtoAddress }) ?? -1
  440. var contactId = 0
  441. if index >= 0 {
  442. contactId = contacts[index]
  443. }
  444. if contactId != 0 && dcContext.getChatIdByContactId(contactId: contactId) != 0 {
  445. showChat(chatId: dcContext.getChatIdByContactId(contactId: contactId), animated: false)
  446. } else {
  447. askToChatWith(address: mailtoAddress)
  448. }
  449. }
  450. }
  451. // MARK: - alerts
  452. private func showDeleteChatConfirmationAlert(chatId: Int) {
  453. let alert = UIAlertController(
  454. title: nil,
  455. message: String.localizedStringWithFormat(String.localized("ask_delete_named_chat"), dcContext.getChat(chatId: chatId).name),
  456. preferredStyle: .safeActionSheet
  457. )
  458. alert.addAction(UIAlertAction(title: String.localized("menu_delete_chat"), style: .destructive, handler: { _ in
  459. self.deleteChat(chatId: chatId, animated: true)
  460. }))
  461. alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
  462. self.present(alert, animated: true, completion: nil)
  463. }
  464. private func askToChatWith(address: String, contactId: Int = 0) {
  465. var contactId = contactId
  466. let alert = UIAlertController(title: String.localizedStringWithFormat(String.localized("ask_start_chat_with"), address),
  467. message: nil,
  468. preferredStyle: .safeActionSheet)
  469. alert.addAction(UIAlertAction(title: String.localized("start_chat"), style: .default, handler: { [weak self] _ in
  470. guard let self = self else { return }
  471. if contactId == 0 {
  472. contactId = self.dcContext.createContact(name: nil, email: address)
  473. }
  474. self.showNewChat(contactId: contactId)
  475. }))
  476. alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: { _ in
  477. if RelayHelper.sharedInstance.isMailtoHandling() {
  478. RelayHelper.sharedInstance.finishMailto()
  479. }
  480. }))
  481. present(alert, animated: true, completion: nil)
  482. }
  483. private func askToChatWith(contactId: Int) {
  484. let dcContact = dcContext.getContact(id: contactId)
  485. askToChatWith(address: dcContact.nameNAddr, contactId: contactId)
  486. }
  487. private func deleteChat(chatId: Int, animated: Bool) {
  488. guard let viewModel = viewModel else { return }
  489. if !animated {
  490. viewModel.deleteChat(chatId: chatId)
  491. refreshInBg()
  492. return
  493. }
  494. if viewModel.searchActive {
  495. viewModel.deleteChat(chatId: chatId)
  496. viewModel.refreshData()
  497. viewModel.updateSearchResults(for: searchController)
  498. return
  499. }
  500. viewModel.deleteChat(chatId: chatId)
  501. }
  502. // MARK: - coordinator
  503. private func showNewChatController() {
  504. let newChatVC = NewChatViewController(dcContext: dcContext)
  505. navigationController?.pushViewController(newChatVC, animated: true)
  506. }
  507. func showChat(chatId: Int, highlightedMsg: Int? = nil, animated: Bool = true) {
  508. if searchController.isActive {
  509. searchController.searchBar.resignFirstResponder()
  510. }
  511. let chatVC = ChatViewController(dcContext: dcContext, chatId: chatId, highlightedMsg: highlightedMsg)
  512. navigationController?.pushViewController(chatVC, animated: animated)
  513. }
  514. public func showArchive(animated: Bool) {
  515. let controller = ChatListController(dcContext: dcContext, isArchive: true)
  516. navigationController?.pushViewController(controller, animated: animated)
  517. }
  518. private func showNewChat(contactId: Int) {
  519. let chatId = dcContext.createChatByContactId(contactId: contactId)
  520. showChat(chatId: Int(chatId))
  521. }
  522. }
  523. // MARK: - uisearchbardelegate
  524. extension ChatListController: UISearchBarDelegate {
  525. func searchBarShouldBeginEditing(_ searchBar: UISearchBar) -> Bool {
  526. viewModel?.beginSearch()
  527. return true
  528. }
  529. func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
  530. // searchBar will be set to "" by system
  531. viewModel?.endSearch()
  532. DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
  533. self.tableView.scrollToTop()
  534. }
  535. }
  536. func searchBar(_ searchBar: UISearchBar, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
  537. tableView.scrollToTop()
  538. return true
  539. }
  540. }