You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

MVVM架构下,如何实现基于API数据的TableView TextField搜索功能?

Got it, let's walk through implementing the search feature in MVVM for your restaurant list—since you already have the basics down (Alamofire fetch, TableView display), this should be a smooth extension. Here's how to structure it properly:

1. Build the ViewModel (MVVM's Logic Hub)

The ViewModel will handle all data operations: fetching from the API, storing raw restaurant data, and filtering based on search input. We'll use Combine for reactive updates (but I'll note a non-Combine alternative too).

import Foundation
import Alamofire
import Combine

class RestaurantViewModel {
    // Store all restaurants from the API (never modified after fetch)
    private var originalRestaurants: [SearchModel] = []
    // Filtered list that feeds the TableView (updated on search)
    @Published var filteredRestaurants: [SearchModel] = []
    
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        // Auto-fetch data on init, or trigger this from your ViewController later
        fetchRestaurants()
    }
    
    // Move your Alamofire request here (ViewController shouldn't handle network calls!)
    func fetchRestaurants() {
        let apiEndpoint = "YOUR_API_URL_HERE"
        AF.request(apiEndpoint)
            .responseDecodable(of: [SearchModel].self) { [weak self] response in
                guard let self = self else { return }
                switch response.result {
                case .success(let restaurants):
                    self.originalRestaurants = restaurants
                    self.filteredRestaurants = restaurants // Show all initially
                case .failure(let error):
                    print("Fetch failed: \(error.localizedDescription)")
                    // Add error handling here (e.g., publish an error state for the ViewController)
                }
            }
    }
    
    // Core search logic: filter original data based on user input
    func searchRestaurants(with query: String) {
        guard !query.trimmingCharacters(in: .whitespaces).isEmpty else {
            filteredRestaurants = originalRestaurants
            return
        }
        // Case-insensitive search for restaurant names
        filteredRestaurants = originalRestaurants.filter { restaurant in
            restaurant.restaurantname?.lowercased().contains(query.lowercased()) ?? false
        }
    }
}

Non-Combine Alternative

If you aren't using Combine, replace the @Published property with a closure callback:

class RestaurantViewModel {
    // ... other properties ...
    var onDataUpdated: (() -> Void)?
    
    // When filteredRestaurants changes, call:
    self.onDataUpdated?()
}

Then in your ViewController, set viewModel.onDataUpdated = { self.restaurantTableView.reloadData() } during setup.

2. Polish Your SearchModel

Let's make the model safe and conform to Decodable (since you're using Alamofire's responseDecodable):

class SearchModel: NSObject, Decodable {
    // Use optional String to avoid force-unwrap crashes
    var restaurantname: String?
    
    // Map API keys to your property names (skip if they match exactly)
    enum CodingKeys: String, CodingKey {
        case restaurantname = "restaurant_name" // Adjust to your API's actual key
    }
    
    // Keep your dictionary init if you need it, alongside Decodable
    init?(dict: [String: Any]) {
        self.restaurantname = dict["restaurantname"] as? String
        super.init()
    }
}

3. Update the ViewController (UI Only!)

Your ViewController's job is only to manage UI and user interactions—no logic allowed. Here's how to wire it up:

import UIKit
import Combine

class RestaurantViewController: UIViewController {
    @IBOutlet weak var searchTextField: UITextField!
    @IBOutlet weak var restaurantTableView: UITableView!
    
    private let viewModel = RestaurantViewModel()
    private var cancellables = Set<AnyCancellable>()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupTableView()
        setupBindings()
        setupSearchListener()
    }
    
    private func setupTableView() {
        restaurantTableView.dataSource = self
        restaurantTableView.register(UITableViewCell.self, forCellReuseIdentifier: "RestaurantCell")
    }
    
    // Listen for changes to the ViewModel's filtered list and refresh the TableView
    private func setupBindings() {
        viewModel.$filteredRestaurants
            .receive(on: DispatchQueue.main) // Ensure UI updates happen on main thread
            .sink { [weak self] _ in
                self?.restaurantTableView.reloadData()
            }
            .store(in: &cancellables)
    }
    
    // Listen for text changes in the search TextField
    private func setupSearchListener() {
        searchTextField.addTarget(self, action: #selector(searchQueryChanged(_:)), for: .editingChanged)
    }
    
    @objc private func searchQueryChanged(_ textField: UITextField) {
        let query = textField.text ?? ""
        viewModel.searchRestaurants(with: query)
    }
}

// TableView DataSource Implementation
extension RestaurantViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return viewModel.filteredRestaurants.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "RestaurantCell", for: indexPath)
        let restaurant = viewModel.filteredRestaurants[indexPath.row]
        cell.textLabel?.text = restaurant.restaurantname ?? "Unknown Restaurant"
        return cell
    }
}

Bonus Tips for Better UX & Performance

  • Debounce Search: If you have a large dataset, add a delay to search calls to avoid filtering on every single keystroke. With Combine, modify the TextField listener to use debounce:
    // Replace setupSearchListener with this Combine-based version
    private func setupSearchListener() {
        searchTextField.publisher(for: \.text)
            .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
            .removeDuplicates()
            .sink { [weak self] query in
                self?.viewModel.searchRestaurants(with: query ?? "")
            }
            .store(in: &cancellables)
    }
    
  • Error Handling: Add an @Published var errorMessage: String? to the ViewModel, then bind it in the ViewController to show an alert when something goes wrong.
  • Empty State: Add a check in the TableView data source to show an empty state cell when filteredRestaurants is empty (either no results or no data fetched).

内容的提问来源于stack exchange,提问作者emmamichael

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.26 09:46:16