Displaying searchable content by using a search controller
At a glance
| Item |
Summary |
| Purpose |
Filters a product catalog by free text and scope while presenting results through UISearchController. |
| App architecture |
MainTableViewController coordinates the source catalog, search controller, results controller, navigation, and restoration state. |
| Main patterns |
Coordinator controller, derived filtered projection, framework delegates, and deferred UI restoration. |
| Project style |
Storyboard UIKit app; one large controller is split into data-source, updating, and restoration extensions. |
Project structure
TableSearch/
├── MainTableViewController.swift
├── MainTableViewController+DataSource.swift
├── MainTableViewController+Updating.swift
├── MainTableViewController+Restore.swift
├── ResultsTableController.swift
├── DetailViewController.swift
├── Product.swift
└── Base.lproj/MainStoryboard.storyboard
Structure observations
- File suffixes divide one controller by responsibility without creating extra owners.
ResultsTableController is intentionally presentation-only; filtering stays with the main controller that owns the complete product array.
Product is in a model file because both table controllers and state restoration use it.
Overall architecture
flowchart LR
Catalog["MainTableViewController.products"] --> MainTable["Main table sections"]
SearchBar["UISearchBar text and scope"] --> Search["UISearchResultsUpdating"]
Catalog --> Search
Search --> Predicate["NSCompoundPredicate"]
Predicate --> Filtered["filteredProducts"]
Filtered --> Results["ResultsTableController"]
MainTable --> Selection["MainTableViewController selection"]
Results --> Selection
Selection --> Detail["DetailViewController"]
Restore["NSCoder state"] --> SearchBar
Reference code
TableSearch/MainTableViewController+Updating.swift:99 — the updater derives a filtered projection and gives it to the results controller.
func updateSearchResults(for searchController: UISearchController) {
// Update the filtered array based on the search text.
let searchResults = products
// Strip out all the leading and trailing spaces.
let whitespaceCharacterSet = CharacterSet.whitespaces
let strippedString =
searchController.searchBar.text!.trimmingCharacters(in: whitespaceCharacterSet)
let searchItems = strippedString.components(separatedBy: " ") as [String]
let andMatchPredicates: [NSPredicate] = searchItems.map { searchString in
findMatches(searchString: searchString)
}
let finalCompoundPredicate =
NSCompoundPredicate(andPredicateWithSubpredicates: andMatchPredicates)
let filteredResults = searchResults.filter { finalCompoundPredicate.evaluate(with: $0) }
if let resultsController = searchController.searchResultsController as? ResultsTableController {
resultsController.filteredProducts = filteredResults
resultsController.tableView.reloadData()
}
}
Interpretation
The full catalog remains the source of truth. Search results are replaceable view state, recalculated from current text and scope rather than mutated back into the source array.
Ownership and state
classDiagram
MainTableViewController *-- UISearchController : creates
MainTableViewController *-- ResultsTableController : storyboard reference
MainTableViewController *-- Product : owns catalog array
MainTableViewController *-- SearchControllerRestorableState : owns pending flags
UISearchController o-- ResultsTableController : presents
ResultsTableController o-- Product : filtered array references
MainTableViewController --> DetailViewController : creates and pushes
DetailViewController o-- Product : selected model
Product ..|> Codable
Ownership evidence
TableSearch/MainTableViewController.swift:36 — the main controller wires and retains the complete search presentation graph.
resultsTableController = storyboard?.instantiateViewController(
withIdentifier: "ResultsTableController") as? ResultsTableController
resultsTableController.tableView.delegate = self
searchController = UISearchController(searchResultsController: resultsTableController)
searchController.delegate = self
searchController.searchResultsUpdater = self
searchController.searchBar.delegate = self
navigationItem.searchController = searchController
| Owner |
State |
Relationship |
Mutation authority |
MainTableViewController |
Full products array |
Owns source catalog |
Data-source setup |
MainTableViewController |
Search controller, result controller, restoration flags |
Retains coordination state |
Lifecycle and delegate callbacks |
ResultsTableController |
filteredProducts |
Owns current projection |
Main search updater replaces it |
DetailViewController |
Selected Product |
Injected before navigation/restoration |
Factory and decode callback |
Class and protocol design
| Type |
Responsibility |
Design note |
MainTableViewController |
Catalog display, search orchestration, selection, and restoration |
Central feature coordinator split across extensions |
ResultsTableController |
Render filtered products |
Passive results data source |
Product |
Searchable/codable product data and formatting |
NSObject and @objc fields support key-path predicates |
DetailViewController |
Render one selected product and restore that selection |
Storyboard factory plus coder hooks |
No app-defined protocol is present. UIKit protocols provide event seams: UISearchResultsUpdating transforms query state, search delegates report UI changes, and table delegates route selection.
Access control
| Symbol |
Access |
Verified effect |
Rationale |
resultsTableController |
private |
Only the main controller file directly reads the backing result controller. |
It is a composition detail; extensions use searchResultsController when needed. |
findMatches(searchString:) |
private |
Only the updating extension can build per-token predicates. |
Prevents predicate construction from becoming feature API. |
| Detail labels and restoration key |
private |
Storyboard outlets and coder key stay inside the detail type. |
They are view/storage implementation details. |
products, searchController, restoredState |
implicit internal |
Same-type extensions in other files can read and update them. |
File-based responsibility split requires module visibility. |
Product properties |
implicit internal @objc |
Swift feature code and Objective-C key-path predicates can access them. |
@objc is interoperability for NSPredicate, not public API. |
Reference code
TableSearch/Product.swift:49 — model fields are exposed to the Objective-C runtime specifically for predicate key paths.
@objc var title: String
@objc var yearIntroduced: Int
@objc var introPrice: Double
@objc var type: Int
Logic ownership and placement
| Logic |
Owner or file |
Why it belongs there |
| Search-controller assembly |
MainTableViewController.swift |
Coordinates navigation and both table presentations. |
| Catalog creation/section mapping |
+DataSource.swift |
Keeps static data and source indexing together. |
| Predicate building/filtering |
+Updating.swift |
Direct implementation of the search-update contract. |
| Search UI encode/decode |
+Restore.swift |
Isolates persistence keys and deferred restoration flags. |
| Result-cell rendering |
ResultsTableController |
Depends only on the filtered projection. |
Design patterns
| Pattern |
Source evidence |
Purpose or tradeoff |
| Search coordinator |
TableSearch/MainTableViewController.swift:30 |
One controller wires the search UI and owns navigation decisions. |
| Filtered projection |
TableSearch/MainTableViewController+Updating.swift:118 |
Keeps the catalog immutable during a query. |
| Extension by responsibility |
TableSearch/MainTableViewController+Restore.swift:10 |
Reduces file size while retaining one state owner. |
| Deferred restoration |
TableSearch/MainTableViewController.swift:71 |
Restores active/first-responder state only after the view appears. |
Naming conventions
Main..., Results..., and Detail... communicate navigation roles.
filteredProducts distinguishes derived state from source products.
RestorationKeys and SearchControllerRestorableState separate serialized keys from temporary replay flags.
- Capability files use
+DataSource, +Updating, and +Restore suffixes.
Architecture takeaways
- Keep query results as a projection of source data rather than a second authoritative model.
- Let one coordinator own
UISearchController, its results screen, and navigation routing.
- When splitting a type across files, accept internal access deliberately and keep truly local helpers private.
- Restore first-responder and presentation state at a lifecycle point where the hierarchy exists.
Source map
| Source file |
Relevant symbols |
TableSearch/MainTableViewController.swift |
Search composition and selection routing |
TableSearch/MainTableViewController+Updating.swift |
Predicate/filter pipeline |
TableSearch/MainTableViewController+DataSource.swift |
Catalog and section mapping |
TableSearch/MainTableViewController+Restore.swift |
Search state restoration |
TableSearch/ResultsTableController.swift |
Filtered-result presentation |
TableSearch/Product.swift |
Searchable/codable model |
TableSearch/DetailViewController.swift |
Detail presentation/restoration |