Skip to content

Fix Search bars on iPadOS 26#5703

Merged
tonisevener merged 31 commits intoliquid-glassfrom
lg-search-ipad-fix
Feb 27, 2026
Merged

Fix Search bars on iPadOS 26#5703
tonisevener merged 31 commits intoliquid-glassfrom
lg-search-ipad-fix

Conversation

@tonisevener
Copy link
Collaborator

@tonisevener tonisevener commented Feb 26, 2026

Phabricator: https://phabricator.wikimedia.org/T418174

Notes

This PR adds a custom back button to the iPadOS 26 search bar, to allow users to easily exit out of the search focused state. I decided to refactor SearchViewController to get the search bars to act a bit more predictably. Now instead of a single SearchViewController class that must handle 3 different presentation contexts, I have split it up into multiple classes that handle each context.

Old

Single SearchViewController class that was used:
- Embedded as the 5th tab in the tab bar controller
- Embedded as the search results controller in Explore and Article's navigationItem.searchController.searchResultsController
- Pushed on or presented individually (SinglePageWebViewController, editor Insert Link and Edit Link flows)

New

SearchResultsViewController

  • Class that is meant to be used ONLY as a navigationItem.searchController.searchResultsController assignment. It is responsible for coordinating display of recently searched terms, languages bar, and the search results (now named SearchResultsListViewController)

SearchTabViewController

  • New class that is embedded as the 5th tab. It assigns SearchResultsViewController as it's navigationItem.searchController.searchResultsController. This class handles the WMFHistoryView.

SearchMinimalViewController

  • New class that is pushed or presented. It assigns SearchResultsViewController as it's navigationItem.searchController.searchResultsController. It is used in SinglePageWebViewController, editor Insert Link and Edit Link flows.

ExploreViewController and ArticleViewController now set SearchResultsViewController as navigationItem.searchController.searchResultsController.

SearchBarIPadCustomizer
This is the class adds the customization for iPadOS 26 search bars. It does not affect iPhones or older iPadOSes, it just serves as a passthrough for them.

SearchResultsViewController uses SearchBarIPadCustomizer to apply the fix to the search bars. I also had to to apply the fix to Places, Saved, and Reading List Detail search bars. Those views do not actually search SearchResultsViewController so they call SearchBarIPadCustomizer directly.

Remaining

I'd like to closely go back over the logging for these to confirm they still work. The hint controllers do not display when saving an article from search results, nor does long press > open in new tab open in a new tab. I will apply these fixes in a followup PR. It may be best to wait on fixing hints until toast rewrites are complete.

Disclaimer

Heavy use of Copilot on this one.

Test Steps

  1. Test searching on iPadOS 26. Test from Explore, Article, Saved tab, Profile > User Page > Search, Editor > insert link and edit link flows. Test search bars in Places, Saved, and Reading List Detail view. Confirm custom back and text clear buttons work well.
  2. Regression test searching on iPhone and lower OSes. Please note anything that feels broken.

Screenshots/Videos

Screenshot 2026-02-25 at 6 32 22 PM

@tonisevener tonisevener requested review from a team and l-olson1214 and removed request for a team February 26, 2026 00:38
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables>
<LocationScenarioReference
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Were the edits in this file intended to be here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, thanks for catching!

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses iPadOS 26 search bar UX issues by introducing an iPad-specific search bar customizer and refactoring the legacy SearchViewController into separate view controllers tailored to each presentation context (tab root vs. embedded search results vs. minimal pushed search).

Changes:

  • Refactor search flow: replace SearchViewController with SearchTabViewController, SearchResultsViewController (as searchResultsController), and SearchMinimalViewController.
  • Add SearchBarIPadCustomizer and wire it into search bars that are affected on iPad (Search, Saved, Places, Reading List Detail).
  • Update History SwiftUI view model/view to support bottom safe-area padding.

Reviewed changes

Copilot reviewed 27 out of 85 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
Wikipedia/Localizations/en.lproj/Localizable.strings Remove deprecated reading-list search placeholder key (en).
Wikipedia/Localizations/qqq.lproj/Localizable.strings Remove deprecated reading-list search placeholder key (qqq).
Wikipedia/Code/WMFAppViewController.m Swap search tab root VC from SearchViewController to SearchTabViewController; update navigation to pushed minimal search.
Wikipedia/Code/WMFAppViewController+Extensions.swift Update tab-selection logging type checks to SearchTabViewController.
Wikipedia/Code/SinglePageWebViewController.swift Use SearchMinimalViewController when pushing search.
Wikipedia/Code/SearchTabViewController.swift New Search tab root controller (history background + system search controller).
Wikipedia/Code/SearchResultsViewController.swift New “searchResultsController-only” container coordinating recent searches, languages bar, and results list + iPad workarounds.
Wikipedia/Code/SearchResultsListViewController.swift Extract the old collection-based results list into its own VC.
Wikipedia/Code/SearchMinimalViewController.swift New lightweight pushed search container.
Wikipedia/Code/SearchBarIPadCustomizer.swift New iPadOS 26 search bar customization helper (custom back + clear).
Wikipedia/Code/SavedViewController.swift Apply iPad customizer to Saved search bar and keep custom clear button in sync.
Wikipedia/Code/ReadingListDetailViewController.swift Remove SearchBarExtendedViewController usage; switch to system search + iPad customizer.
Wikipedia/Code/PlacesViewController.swift Apply iPad customizer to Places search bar and keep custom clear button in sync.
Wikipedia/Code/NSError+WMFExtensions.h / .m Add wmf_isCancelledError helper.
Wikipedia/Code/InsertLinkViewController.swift Use SearchResultsViewController as the searchResultsController for link insertion search.
Wikipedia/Code/HintController.swift Update keyboard-hiding special case to new search results list VC type.
Wikipedia/Code/ExploreViewController.swift Use SearchResultsViewController as embedded searchResultsController and wire up navigation callbacks.
Wikipedia/Code/EditLinkViewController.swift Use SearchMinimalViewController for edit-link article search.
Wikipedia/Code/ArticleViewController.swift Replace embedded search results controller with SearchResultsViewController; adjust iPadOS 26 tab bar behavior.
Wikipedia.xcodeproj/xcshareddata/xcschemes/Wikipedia.xcscheme Enable location simulation + set a default location scenario.
Wikipedia.xcodeproj/project.pbxproj Add new search-related files; remove some old search-related entries (but still contains stale SearchViewController.swift references).
WMFComponents/Sources/WMFComponents/Components/History/WMFHistoryViewModel.swift Add bottomPadding to support safe-area layout.
WMFComponents/Sources/WMFComponents/Components/History/WMFHistoryView.swift Use content margins for top/bottom padding instead of header padding.

searchBarIPadCustomizer.onClearTapped = { [weak self] searchController in
guard let self,
let searchBar = searchController?.searchBar else { return }
self.searchBar(searchBar, textDidChange: "")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: textDidChange taking "" feels odd. Can we make it take nil, have a different name, etc?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I agree it's weird - for some reason programmatically clearing the search bar text (which I do upon clear button tap) is not triggering the UISearchBarDelegate textDidChange method, which is a system method that takes a non-optional search term. So I'm calling it manually here, but it does feel like I must be doing something wrong. Open any ideas here!

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 26 out of 84 changed files in this pull request and generated 3 comments.

Comment on lines +20 to 24
self?.readingListEntryCollectionViewController.updateSearchString("")
}
return customizer
}()

Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SearchBarIPadCustomizer’s custom back button only deactivates the UISearchController; in this VC no onBackTapped/onWillDismiss handler is set to clear the text and reset the filtering state. Previously the cancel/resign flow explicitly cleared the search text and called updateSearchString(""), so on iPadOS 26 the new back button can leave the reading list permanently filtered after exiting the focused search state. Consider wiring searchiPadCustomizer.onBackTapped (and/or onWillDismiss) to clear the search bar text and reset readingListEntryCollectionViewController.updateSearchString("").

Suggested change
self?.readingListEntryCollectionViewController.updateSearchString("")
}
return customizer
}()
self?.clearSearch()
}
customizer.onBackTapped = { [weak self] _ in
self?.clearSearch()
}
customizer.onWillDismiss = { [weak self] _ in
self?.clearSearch()
}
return customizer
}()
private func clearSearch() {
navigationItem.searchController?.searchBar.text = ""
readingListEntryCollectionViewController.updateSearchString("")
}

Copilot uses AI. Check for mistakes.
Comment on lines 293 to 304
fetcher.fetchArticles(forSearchTerm: searchTerm, siteURL: siteURL, resultLimit: WMFMaxSearchResultLimit, failure: { error in
failure(error, .prefix)
}, success: { [weak self] results in
guard let self else { return }
success(results, .prefix)
guard let resultsArray = results.results, resultsArray.count < 12 else { return }
self.fetcher.fetchArticles(forSearchTerm: searchTerm, siteURL: siteURL, resultLimit: WMFMaxSearchResultLimit, fullTextSearch: true, appendToPreviousResults: results, failure: { error in
failure(error, .full)
}, success: { [weak self] fullTextResults in
guard self != nil else { return }
success(fullTextResults, .full)
})
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the prefix + full-text search flow, if the full-text request fails you call failure(error, .full), which clears resultsViewController.results and sets an empty state. That will wipe already-successful prefix results and show “no results / no internet” even though the user still has valid prefix matches. Consider keeping the existing prefix results on .full failure (e.g., only log the error, or only apply the failure UI when there are currently no results).

Copilot uses AI. Check for mistakes.
@objc private func userDidSaveOrUnsaveArticle(_ notification: Notification) {
guard let article = notification.object as? WMFArticle else { return }
let context: [String: Any] = [ReadingListHintController.ContextArticleKey: article]
hintController?.toggle(presenter: self, context: context, theme: theme)
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

userDidSaveOrUnsaveArticle(_:) always toggles the reading-list hint with presenter: self. When a user saves/unsaves from a VC pushed on top of the Search tab’s navigation stack (e.g. an article opened from history/search), SearchTabViewController won’t be the visible presenter, so the hint can fail to display or appear behind the top VC. Consider presenting from the currently visible view controller (e.g. navigationController?.topViewController as? (UIViewController & HintPresenting)) similar to the previous visibleHintPresentingViewController logic.

Suggested change
hintController?.toggle(presenter: self, context: context, theme: theme)
let presenter = (navigationController?.topViewController as? (UIViewController & HintPresenting)) ?? self
hintController?.toggle(presenter: presenter, context: context, theme: theme)

Copilot uses AI. Check for mistakes.
@tonisevener tonisevener merged commit 9097211 into liquid-glass Feb 27, 2026
3 checks passed
@tonisevener tonisevener deleted the lg-search-ipad-fix branch February 27, 2026 00:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

4 participants