Swift开发:如何将Model数据传至TableViewController再到DetailViewController
现有代码问题修复
首先修正你现有代码中的错误,保证基础逻辑可运行:
1. 修正Constants结构体
修复接口硬编码问题,将固定参数改为动态传参:
import Foundation struct Constants { struct Urls { static let mealsCategoriesUrl = URL(string: "https://www.themealdb.com/api/json/v1/1/categories.php")! // 动态传入分类名获取对应餐食列表 static func getMealNameCategoryUrl(_ category: String) -> URL { return URL(string:"https://www.themealdb.com/api/json/v1/1/filter.php?c=\(category)")! } // 动态传入餐食ID获取详情 static func getMealByIdUrl(_ idMeal: String) -> URL { return URL(string:"https://www.themealdb.com/api/json/v1/1/lookup.php?i=\(idMeal)")! } } }
2. 修正模型定义
修复递归错误、字段不匹配问题,适配theMealDB接口返回结构:
// 分类列表返回模型 struct MealCategoryResponse: Decodable { let categories: [MealCategory] } struct MealCategory: Decodable { let idCategory: String let strCategory: String let strCategoryThumb: String let strCategoryDescription: String } // 餐食列表返回模型 struct MealsResponse: Decodable { let meals: [Meal] } struct Meal: Decodable { let strMeal: String let strMealThumb: String let idMeal: String } // 餐食详情返回模型 struct MealDetailModel: Decodable { let meals: [MealDetail] } struct MealDetail: Decodable { let idMeal: String let strMeal: String let strCategory: String let strInstructions: String let strMealThumb: String let strYoutube: String // 适配接口多食材字段 let strIngredient1: String? let strIngredient2: String? let strIngredient3: String? let strMeasure1: String? let strMeasure2: String? let strMeasure3: String? // 可按需继续添加到strIngredient20/strMeasure20 }
3. 修正ViewModel
修复方法作用域、逻辑嵌套问题:
import Foundation import UIKit import Combine // 分类列表ViewModel @MainActor class MealsListModel: ObservableObject { @Published public var meals: [Meal] = [] // 加载指定分类下的所有餐食 public func loadMeals(category: String) async { do { let mealResponse = try await WebServiceRequest().parseData(url: Constants.Urls.getMealNameCategoryUrl(category)) { data in return try? JSONDecoder().decode(MealsResponse.self, from: data) } self.meals = mealResponse.meals } catch { print(error) } } } // 餐食项ViewModel struct MealViewModel: Identifiable { let id = UUID() private let meal: Meal init(_ meal: Meal) { self.meal = meal } var idMeal: String { meal.idMeal } var strMeal: String { meal.strMeal } var strMealThumb: URL? { URL(string: meal.strMealThumb) } } // 详情页ViewModel @MainActor class DetailViewModel: ObservableObject{ @Published var strMeal: String = "" @Published var strInstructions: String = "" @Published var ingredients: [String] = [] @Published var strMealThumb:URL? // 加载指定ID的餐食详情 public func loadMealDetails(idMeal: String) async { do { let mealDetailResponse = try await WebServiceRequest().parseData(url: Constants.Urls.getMealByIdUrl(idMeal)) { data in return try? JSONDecoder().decode(MealDetailModel.self, from: data) } guard let detail = mealDetailResponse.meals.first else { return } self.strMeal = detail.strMeal self.strInstructions = detail.strInstructions self.strMealThumb = URL(string: detail.strMealThumb) // 拼接食材+用量 var tempIngredients: [String] = [] if let i1 = detail.strIngredient1, let m1 = detail.strMeasure1, !i1.isEmpty { tempIngredients.append("\(m1) \(i1)") } if let i2 = detail.strIngredient2, let m2 = detail.strMeasure2, !i2.isEmpty { tempIngredients.append("\(m2) \(i2)") } // 按需继续添加后续食材拼接逻辑 self.ingredients = tempIngredients } catch { print(error) } } }
实现餐食列表TableViewController
import UIKit import Combine class MealsTableViewController: UITableViewController { private let viewModel = MealsListModel() private var cancellables = Set<AnyCancellable>() private var mealList: [MealViewModel] = [] override func viewDidLoad() { super.viewDidLoad() title = "餐食列表" // 注册cell tableView.register(UITableViewCell.self, forCellReuseIdentifier: "MealCell") // 订阅ViewModel数据变化 viewModel.$meals .receive(on: DispatchQueue.main) .sink { [weak self] meals in self?.mealList = meals.map(MealViewModel.init) self?.tableView.reloadData() } .store(in: &cancellables) // 加载Beef分类下的餐食,可按需修改分类参数 Task { await viewModel.loadMeals(category: "Beef") } } // MARK: - Table view data source override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return mealList.count } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "MealCell", for: indexPath) let meal = mealList[indexPath.row] cell.textLabel?.text = meal.strMeal // 异步加载缩略图 cell.imageView?.image = UIImage(systemName: "photo") if let thumbUrl = meal.strMealThumb { Task { if let (data, _) = try? await URLSession.shared.data(from: thumbUrl) { cell.imageView?.image = UIImage(data: data) } } } return cell } // MARK: - 点击跳转详情 override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let selectedMeal = mealList[indexPath.row] let detailVC = DetailViewController() detailVC.mealId = selectedMeal.idMeal navigationController?.pushViewController(detailVC, animated: true) } }
实现详情页DetailViewController
import UIKit import Combine class DetailViewController: UIViewController { var mealId: String = "" private let viewModel = DetailViewModel() private var cancellables = Set<AnyCancellable>() // 详情页UI元素 private let mealImageView: UIImageView = { let iv = UIImageView() iv.contentMode = .scaleAspectFill iv.clipsToBounds = true iv.layer.cornerRadius = 8 return iv }() private let nameLabel: UILabel = { let label = UILabel() label.font = .boldSystemFont(ofSize: 20) label.numberOfLines = 0 return label }() private let ingredientsLabel: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 16) label.numberOfLines = 0 return label }() private let instructionsLabel: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 16) label.numberOfLines = 0 return label }() override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .white setupUI() // 订阅数据变化 viewModel.$strMeal .assign(to: \.text, on: nameLabel) .store(in: &cancellables) viewModel.$ingredients .receive(on: DispatchQueue.main) .sink { [weak self] ingredients in self?.ingredientsLabel.text = "食材:\n" + ingredients.joined(separator: "\n") } .store(in: &cancellables) viewModel.$strInstructions .receive(on: DispatchQueue.main) .sink { [weak self] instructions in self?.instructionsLabel.text = "做法:\n\(instructions)" } .store(in: &cancellables) viewModel.$strMealThumb .receive(on: DispatchQueue.main) .sink { [weak self] url in guard let url = url else { return } Task { if let (data, _) = try? await URLSession.shared.data(from: url) { self?.mealImageView.image = UIImage(data: data) } } } .store(in: &cancellables) // 加载详情数据 Task { await viewModel.loadMealDetails(idMeal: mealId) } } private func setupUI() { let scrollView = UIScrollView() let stackView = UIStackView(arrangedSubviews: [mealImageView, nameLabel, ingredientsLabel, instructionsLabel]) stackView.axis = .vertical stackView.spacing = 16 stackView.translatesAutoresizingMaskIntoConstraints = false scrollView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(scrollView) scrollView.addSubview(stackView) NSLayoutConstraint.activate([ scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), stackView.topAnchor.constraint(equalTo: scrollView.topAnchor), stackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), stackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor), mealImageView.heightAnchor.constraint(equalToConstant: 200) ]) } }
内容的提问来源于stack exchange,提问作者Wen Love Howard
相关产品推荐
相关产品推荐

