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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.26 01:54:03