Reputation: 384
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:
Upvotes: 0
Views: 4376
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
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