Swift3中基于MVVM实现TextField触发TableView搜索的方法
Great question! Implementing search with MVVM in UIKit means keeping all data filtering logic in your ViewModel, while the ViewController handles UI interactions and updates. Let's break this down step by step, building on your existing ViewModel code.
Step 1: Update the ViewModel
We'll add properties to manage filtered data, the search query, and a way to notify the ViewController when data changes. I'll show two approaches—one using a closure for simple cases, and another using Combine for reactive updates (iOS 13+).
Option A: Closure-based approach (compatible with older iOS versions)
class ViewModel: NSObject { var datasourceModel: DataSourceModel // Holds the filtered results for display private(set) var filteredModels: [Model] = [] // Closure to notify the View when data updates var onDataUpdated: (() -> Void)? // Search text that triggers filtering when changed var searchText: String = "" { didSet { filterData() } } init(withdatasource newDatasourceModel: DataSourceModel) { datasourceModel = newDatasourceModel super.init() // Initialize with full dataset filteredModels = datasourceModel.models // Assume your DataSourceModel has a `models` array of [Model] onDataUpdated?() } // Core filtering logic—adjust based on your Model's properties private func filterData() { guard !searchText.isEmpty else { // Reset to full data when search is empty filteredModels = datasourceModel.models onDataUpdated?() return } // Example: Filter by model's name (case-insensitive) filteredModels = datasourceModel.models.filter { model in model.name.lowercased().contains(searchText.lowercased()) } onDataUpdated?() } // Updated method to get display data (uses filtered results) func dataForDisplay(at indexPath: IndexPath) -> Model { return filteredModels[indexPath.row] } // Helper to get row count for TableView func numberOfRows() -> Int { return filteredModels.count } }
Option B: Combine-based approach (reactive, iOS 13+)
If you're targeting iOS 13 or later, using Combine makes the data flow cleaner and adds nice features like debouncing to avoid frequent filtering:
import Combine class ViewModel: NSObject { var datasourceModel: DataSourceModel @Published private(set) var filteredModels: [Model] = [] @Published var searchText: String = "" private var cancellables = Set<AnyCancellable>() init(withdatasource newDatasourceModel: DataSourceModel) { datasourceModel = newDatasourceModel super.init() filteredModels = datasourceModel.models setupBindings() } private func setupBindings() { $searchText // Add a 300ms delay to avoid filtering on every keystroke .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main) .sink { [weak self] text in self?.filterData(with: text) } .store(in: &cancellables) } private func filterData(with text: String) { guard !text.isEmpty else { filteredModels = datasourceModel.models return } filteredModels = datasourceModel.models.filter { $0.name.lowercased().contains(text.lowercased()) } } func dataForDisplay(at indexPath: IndexPath) -> Model { return filteredModels[indexPath.row] } func numberOfRows() -> Int { return filteredModels.count } }
Step 2: Update the ViewController
Now we'll connect the TextField to the ViewModel, and link the TableView to the filtered data.
Closure-based ViewController
class ViewController: UIViewController { // UI Elements private let searchTextField = UITextField() private let tableView = UITableView() // ViewModel instance private var viewModel: ViewModel! override func viewDidLoad() { super.viewDidLoad() setupUI() setupViewModel() setupTextField() setupTableView() } private func setupUI() { // Add and layout UI elements (using auto layout) searchTextField.placeholder = "Search..." searchTextField.borderStyle = .roundedRect tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell") view.addSubview(searchTextField) view.addSubview(tableView) searchTextField.translatesAutoresizingMaskIntoConstraints = false tableView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ searchTextField.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16), searchTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), searchTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), tableView.topAnchor.constraint(equalTo: searchTextField.bottomAnchor, constant: 16), tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), tableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor) ]) } private func setupViewModel() { // Replace with your actual DataSourceModel initialization (from your API load) let dataSourceModel = DataSourceModel() viewModel = ViewModel(withdatasource: dataSourceModel) // Listen for data updates to reload TableView viewModel.onDataUpdated = { [weak self] in DispatchQueue.main.async { self?.tableView.reloadData() } } } private func setupTextField() { // Update ViewModel's searchText when TextField changes searchTextField.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged) } private func setupTableView() { tableView.dataSource = self tableView.delegate = self } @objc private func textFieldDidChange() { viewModel.searchText = searchTextField.text ?? "" } } // MARK: - UITableView DataSource & Delegate extension ViewController: UITableViewDataSource, UITableViewDelegate { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return viewModel.numberOfRows() } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) let model = viewModel.dataForDisplay(at: indexPath) cell.textLabel?.text = model.name // Replace with your Model's display property return cell } }
Combine-based ViewController
If you used the Combine ViewModel, adjust the setupViewModel method and add a cancellables set:
class ViewController: UIViewController { // ... (same UI setup as above) private var cancellables = Set<AnyCancellable>() private func setupViewModel() { let dataSourceModel = DataSourceModel() viewModel = ViewModel(withdatasource: dataSourceModel) // Subscribe to filteredModels changes viewModel.$filteredModels .receive(on: DispatchQueue.main) .sink { [weak self] _ in self?.tableView.reloadData() } .store(in: &cancellables) } // ... (rest of the code remains the same) }
Key Notes
- MVVM Separation: All filtering logic lives in the ViewModel—your ViewController doesn't know anything about how data is filtered, it just displays what the ViewModel provides.
- Filtering Logic: Adjust the
filterDatamethod to search the properties relevant to yourModel(e.g., description, ID, etc.). - Retain Cycles: Always use
[weak self]in closures/subscriptions to avoid memory leaks. - Debouncing: The Combine approach includes a debounce to prevent excessive filtering while the user is typing—you can add this to the closure approach using a timer if needed.
内容的提问来源于stack exchange,提问作者robbieroberts

