Swift - display specific data for each tableview section

久未见 提交于 2021-01-29 08:33:45

问题


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:


回答1:


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()
}



回答2:


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.



来源:https://stackoverflow.com/questions/62909423/swift-display-specific-data-for-each-tableview-section

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!