Sample CodeiOS, iPadOS, Mac CatalystReviewed 2026-07-21View on Apple Developer

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

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

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