【Xcode/Swift】RxDataSourcesを使ってレシピ表を作ってみる

View側 (Storyboard, TableViewCell)

HomeViewController.swift (メインのViewController)

RecipeDetailViewController.swift (遷移先のViewController)

RecipeTableViewCell.swift (TableViewCell)


ロジック側 (Model, ViewController)

Recipe.swift

import Foundation

// レシピの必須情報、これを以下のクラスにそれぞれ準拠させる
protocol Recipe {
    var name: String { get }
    var ingredients: [String] { get }
    var instructions: String { get }
}

class PancakeRecipe: Recipe {
    var name = "Pancakes"
    var ingredients = ["flour", "milk", "eggs", "butter"]
    var instructions = "1. Mix flour and milk. \n2. Add eggs and melted butter. \n3. Cook on griddle until golden brown."
}

class SpaghettiRecipe: Recipe {
    var name = "Spaghetti"
    var ingredients = ["spaghetti noodles", "tomato sauce", "ground beef", "onion"]
    var instructions = "1. Cook spaghetti noodles according to package directions. \n2. Brown ground beef and onion. \n3. Add tomato sauce and simmer. \n4. Serve over cooked spaghetti."
}

class OmletteRecipe: Recipe {
    var name = "Omlette"
    var ingredients = ["eggs", "cheese", "onion", "bell pepper", "mushrooms"]
    var instructions = "1. Beat eggs. \n2. Add remaining ingredients. \n3. Cook in pan until done."
}

class MisoRamenRecipe: Recipe {
    var name = "Miso Ramen"
    var ingredients = ["ramen noodles", "miso paste", "chicken broth", "green onions", "mushrooms", "bean sprouts", "pork"]
    var instructions = "1. Cook ramen noodles according to package directions. \n2. Heat chicken broth and miso paste. \n3. Add ramen noodles and toppings."
}

class GrilledChickenRecipe: Recipe {
    var name = "Grilled Chicken"
    var ingredients = ["chicken breasts", "olive oil", "lemon juice", "garlic", "salt", "pepper"]
    var instructions = "1. Preheat grill to medium-high heat. \n2. In a bowl, mix olive oil, lemon juice, minced garlic, salt, and pepper. \n3. Coat chicken breasts with the marinade. \n4. Grill chicken for 6-8 minutes per side, or until cooked through."
}

class CaesarSaladRecipe: Recipe {
    var name = "Caesar Salad"
    var ingredients = ["romaine lettuce", "croutons", "Parmesan cheese", "Caesar dressing"]
    var instructions = "1. Wash and dry romaine lettuce, then tear into bite-sized pieces. \n2. Toss lettuce with croutons and Parmesan cheese. \n3. Drizzle Caesar dressing over the salad and toss to coat evenly."
}

class ChocolateChipCookieRecipe: Recipe {
    var name = "Chocolate Chip Cookies"
    var ingredients = ["butter", "sugar", "brown sugar", "eggs", "vanilla extract", "flour", "baking soda", "salt", "chocolate chips"]
    var instructions = "1. Preheat oven to 375°F (190°C). \n2. In a mixing bowl, cream together butter, sugar, and brown sugar. \n3. Beat in eggs and vanilla extract. \n4. In a separate bowl, combine flour, baking soda, and salt. Gradually add to the butter mixture. \n5. Stir in chocolate chips. \n6. Drop rounded tablespoons of dough onto a greased baking sheet. \n7. Bake for 9-11 minutes or until golden brown."
}

class ChickenCurryRecipe: Recipe {
    var name = "Chicken Curry"
    var ingredients = ["chicken", "onion", "garlic", "ginger", "curry powder", "coconut milk", "tomato", "salt", "pepper"]
    var instructions = "1. Heat oil in a large pan over medium heat. \n2. Add chopped onion, minced garlic, and grated ginger. Sauté until fragrant. \n3. Add chicken pieces and cook until browned. \n4. Stir in curry powder and cook for a minute. \n5. Add coconut milk and diced tomato. Season with salt and pepper. \n6. Simmer for 20-25 minutes, or until the chicken is cooked through. Serve with rice or naan bread."
}

class VegetableStirFryRecipe: Recipe {
    var name = "Vegetable Stir Fry"
    var ingredients = ["mixed vegetables (e.g., bell peppers, broccoli, carrots)", "onion", "garlic", "soy sauce", "sesame oil", "cornstarch", "vegetable oil"]
    var instructions = "1. Heat vegetable oil in a large skillet or wok over high heat. \n2. Add chopped onion and minced garlic. Stir-fry for a minute. \n3. Add mixed vegetables and cook until tender-crisp. \n4. In a small bowl, whisk together soy sauce, sesame"
}

RecipeTableViewCell.swift (TableViewのセル)

import UIKit

class RecipeTableViewCell: UITableViewCell {

    @IBOutlet private weak var recipeNameLabel: UILabel!

    override func prepareForReuse() {
        super.prepareForReuse()
        recipeNameLabel.text = nil
    }

    func configure(recipe: String) {
        recipeNameLabel.text = recipe
    }

}

HomeViewController.swift

import UIKit
import RxSwift
import RxCocoa
import RxDataSources

struct RecipeSectionModel {
    var header: String
    var items: [Recipe]
}

class HomeViewController: UIViewController {

    @IBOutlet private weak var tableView: UITableView!

    private let recipes: [Recipe] = [PancakeRecipe(), SpaghettiRecipe(), OmletteRecipe(), MisoRamenRecipe(), GrilledChickenRecipe(), CaesarSaladRecipe(), ChocolateChipCookieRecipe(), ChickenCurryRecipe(), VegetableStirFryRecipe()]

    private let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()
        setupTableView()
    }

    private func setupTableView() {
        // カスタムTableViewCellを使ってリストでレシピを表示させるための設定を行う
        let dataSource = RxTableViewSectionedReloadDataSource<RecipeSectionModel>(configureCell: { dataSource, tableView, indexPath, recipe in
            guard let cell = tableView.dequeueReusableCell(withIdentifier: "RecipeTableViewCell", for: indexPath) as? RecipeTableViewCell else {
                return UITableViewCell()
            }
            cell.configure(recipe: recipe.name)
            return cell
        })

        let sections = Observable.just([RecipeSectionModel(header: "Recipes", items: recipes)])

        tableView.rx.setDelegate(self).disposed(by: disposeBag)

        sections
            .bind(to: tableView.rx.items(dataSource: dataSource))
            .disposed(by: disposeBag)

        // セルがタップされた時に画面遷移をする
        tableView.rx.modelSelected(Recipe.self)
            .subscribe(onNext: { [weak self] recipe in
                guard let self else { return }
                let recipeDetailViewController = RecipeDetailViewController(recipe: recipe)
                self.navigationController?.pushViewController(recipeDetailViewController, animated: true)
            })
            .disposed(by: disposeBag)

        tableView.register(UINib(nibName: "RecipeTableViewCell", bundle: nil), forCellReuseIdentifier: "RecipeTableViewCell")
    }

}

extension HomeViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 80
    }
}

extension RecipeSectionModel: SectionModelType {
    typealias Item = Recipe

    init(original: RecipeSectionModel, items: [Item]) {
        self = original
        self.items = items
    }
}

RecipeDetailViewController.swift

import UIKit

class RecipeDetailViewController: UIViewController {

    @IBOutlet private weak var nameLabel: UILabel!
    @IBOutlet private weak var ingredientsLabel: UILabel!
    @IBOutlet private weak var instructionsTextView: UITextView!

    private let recipe: Recipe

    init(recipe: Recipe) {
        self.recipe = recipe
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        setupRecipe()
    }

    private func setupRecipe() {
        nameLabel.text = recipe.name
        ingredientsLabel.text = recipe.ingredients.joined(separator: ", ")
        instructionsTextView.text = recipe.instructions
    }

}