Veronika Babii
Veronika Babii

Reputation: 384

Display specific data for each tableview section

I'm working with CocktailDB. By creating a request I get a JSON file, parse it with Decodable protocol. From JSON I get all drinks' categories and display them as the sections of my tableview.

In each tableview section I want to display drinks from specific category (section's header). One drink per section cell from the category (drink's strDrink (name) and strDrinkThumb (image)).

I have a method that creates a request to get drinks from specific category - getDrinksFrom(category: String).
Please advice how can I call this method for specific section to get and display drinks from specific category in this section?

My code:

class ViewController: UIViewController {
    
    var drinks = [Drink]()
    var categories = [Category]()
    
    @IBOutlet weak var tableView: UITableView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        getCategories()
        getDrinksFrom(category: "Cocoa")
    }
    
    func getCategories() {
        let url = URL(string: "https://www.thecocktaildb.com/api/json/v1/1/list.php?c=list")
        
        URLSession.shared.dataTask(with: url!) { (data, response, error) in
            
            if error == nil {
                do {
                    self.categories = try JSONDecoder().decode(Categories.self, from: data!).drinks
                    
                    DispatchQueue.main.async {
                        self.tableView.reloadData()
                    }
                    print(self.categories)
                    
                } catch {
                    print(error)
                }
            }
        }.resume()
    }
    
    func getDrinksFrom(category: String) {
        let url = URL(string: "https://www.thecocktaildb.com/api/json/v1/1/filter.php?c=\(category)")
        
        URLSession.shared.dataTask(with: url!) { (data, response, error) in
            
            if error == nil {
                do {
                    self.drinks = try JSONDecoder().decode(Drinks.self, from: data!).drinks
                    
                    DispatchQueue.main.async {
                        self.tableView.reloadData()
                    }
                    print(self.drinks)
                    
                } catch {
                    print(error)
                }
            }
        }.resume()
    }
    
}

extension ViewController: UITableViewDataSource, UITableViewDelegate {
    
    func numberOfSections(in tableView: UITableView) -> Int {
        return categories.count
    }
    
    func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        return categories[section].strCategory
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 2
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "drinkCell") as! DrinkCell
        
        cell.drinkName.text = drinks[indexPath.row].strDrink
        
        let url = drinks[indexPath.row].strDrinkThumb
        cell.drinkImage.downloaded(from: url)
        
        return cell
    }
}

// to download an image from web
extension UIImageView {
    func downloaded(from url: URL, contentMode mode: UIView.ContentMode = .scaleAspectFit) {
        contentMode = mode
        URLSession.shared.dataTask(with: url) { data, response, error in
            guard
                let httpURLResponse = response as? HTTPURLResponse, httpURLResponse.statusCode == 200,
                let mimeType = response?.mimeType, mimeType.hasPrefix("image"),
                let data = data, error == nil,
                let image = UIImage(data: data)
                else { return }
            DispatchQueue.main.async() { [weak self] in
                self?.image = image
            }
        }.resume()
    }
    
    func downloaded(from link: String, contentMode mode: UIView.ContentMode = .scaleAspectFit) {
        guard let url = URL(string: link) else { return }
        downloaded(from: url, contentMode: mode)
    }
}

Category Model:

struct Categories:Decodable {
    var drinks: [Category]
}

struct Category:Decodable {
    var strCategory: String
}

Drink Model:

struct Drinks:Decodable {
    var drinks: [Drink]
}

struct Drink:Decodable {
    var strDrink: String
    var strDrinkThumb: String
}

What I have for know:

JSON structure: enter image description here

Upvotes: 0

Views: 4376

Answers (2)

vadian
vadian

Reputation: 285069

My suggestion is to create a custom struct Category with name and drinks for the sections. It does not conform to Decodable, this is intended

struct Category {
    let name : String
    var drinks : [Drink]
}

and an appropriate data source array

var categories = [Category]()

then load and parse the categories with traditional JSONSerialization and populate the array by mapping the names. Further add a completion handler

func getCategories(completion: @escaping () -> Void) {
    let url = URL(string: "https://www.thecocktaildb.com/api/json/v1/1/list.php?c=list")
    
    URLSession.shared.dataTask(with: url!) { (data, response, error) in
        
        if let error = error { print(error); return }
        do {
            let result = try JSONSerialization.jsonObject(with: data!) as! [String:Any]
            let categoryNames = result["drinks"] as! [[String:String]]
            self.categories = categoryNames.map{ Category(name: $0["strCategory"]!, drinks:[])}
            completion()
            
        } catch {
            print(error)
        }
    }.resume()
}

To avoid naming confusion (too many drinks) name the root struct Response

struct Response : Decodable {
    let drinks: [Drink]
}

Load the data related to a category and assign the drinks array to the corresponding array in categories

func getDrinksFrom(category: String) {
    let url = URL(string: "https://www.thecocktaildb.com/api/json/v1/1/filter.php?c=\(category)")
    
    URLSession.shared.dataTask(with: url!) { (data, response, error) in
        
        if let error = error { print(error); return }
        do {
            let drinks = try JSONDecoder().decode(Response.self, from: data!).drinks
            guard let index = categories.firstIndex(where: {$0.name == category}) else { return }
            self.categories[index].drinks = drinks
            DispatchQueue.main.async {
                self.tableView.reloadData()
            }
            
        } catch {
            print(error)
        }
    }.resume()
}

and replace viewDidLoad with

override func viewDidLoad() {
    super.viewDidLoad()
    getCategories { [weak self] in
        self?.getDrinksFrom(category: "Cocoa")
    }
}

Finally change the table view data source methods to match the section structure

extension ViewController: UITableViewDataSource, UITableViewDelegate {
    
    func numberOfSections(in tableView: UITableView) -> Int {
        return categories.count
    }
    
    func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        return categories[section].name
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return categories[section].drinks.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "drinkCell") as! DrinkCell
        
        let category = categories[indexPath.section]
        let drink = category.drinks[indexPath.row]
        cell.drinkName.text = drink.strDrink
        
        let url = drink.strDrinkThumb
        cell.drinkImage.downloaded(from: url)
        
        return cell
    }
}

You can also put both functions together and load all drinks for all categories

func loadAllCategories() {
    let url = URL(string: "https://www.thecocktaildb.com/api/json/v1/1/list.php?c=list")
    
    URLSession.shared.dataTask(with: url!) { (data, response, error) in
        
        if let error = error { print(error); return }
        do {
            let result = try JSONSerialization.jsonObject(with: data!) as! [String:Any]
            let categoryNames = (result["drinks"] as! [[String:String]]).map{$0["strCategory"]!}
            let group = DispatchGroup()
            for category in categoryNames {
                let categoryURLString = "https://www.thecocktaildb.com/api/json/v1/1/filter.php?c=\(category)".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
                let categoryURL = URL(string: categoryURLString)!
                group.enter()
                let categoryTask = URLSession.shared.dataTask(with: categoryURL) { (categoryData, _, categoryError) in
                    defer { group.leave() }
                    if let categoryError = categoryError { print(categoryError); return }
                    do {
                        let drinks = try JSONDecoder().decode(Response.self, from: categoryData!).drinks
                        self.categories.append(Category(name: category, drinks: drinks))
                    } catch {
                        print(error)
                    }
                }
                categoryTask.resume()
                
            }
            group.notify(queue: .main) {
                self.tableView.reloadData()
            }
            
        } catch {
            print(error)
        }
    }.resume()
}

Upvotes: 2

Vinaykrishnan
Vinaykrishnan

Reputation: 768

This is just a pseudocode, which will give you an idea how you can proceed further. The code has not been tested.

Create an array of sections to be loaded.

var sections: [Sections] = []

In you tableview delegates you can create a struct for the sections that you need to load, which will help you to identify the section in cell for row index path where you can call API based on categories.

extension ViewController: UITableViewDataSource, UITableViewDelegate {
    
    struct Sections {
        static var count = 0
        // In stantiate table view headers index order
        enum SectionType {
            case SoftDrink
            case OrdinaryDrink
            case MilkShake
        }
        
        var type: SectionType?
        var section: Int?
        var rows: Int?
    }
    
    func setUpTableView() {
        // Set Up Tableview Data
        if check if Drink is type of SoftDrink /*If you sections are loaded dynamic u can add condition*/ {
            sections.append(Sections(type: .SoftDrink, section: Sections.count, rows: 1))
            Sections.count += 1
        }
        Sections.count = 0
    }

    
    func numberOfSections(in _: UITableView) -> Int {
        sections.count
    }
    
    func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int {
        sections[section].rows ?? 0
    }
    
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        var tableCell: UITableViewCell = UITableViewCell()
        guard let type = sections[indexPath.section].type else {
            tableCell.selectionStyle = .none
            return tableCell
        }
        switch type {
        case .SoftDrink: break
        // Instantiate cell and API calls.
        case .OrdinaryDrink: break
        // Instantiate cell and API calls.
        case .MilkShake: break
            // Instantiate cell and API calls.
        }
        tableCell.selectionStyle = .none
        
        return tableCell
    }
    
}

setUpTableView() can be called in viewDidLoad Method.

Upvotes: 0

Related Questions