2

Good afternoon. I am working on my final year project, in my project I am using Google Maps API to show results in CollectionView.

If I test print the array, result is successful and I get the data displayed. Sometimes the app works perfectly, If I run it, it goes through and working, 75% of the times I get Fatal error: Index out of range: file Swift/ContiguousArrayBuffer.swift, line 444.

Any help is so much appreciated and thank you so much.

    import Foundation

// MARK: - BloodBanksData
struct BloodBanksData: Codable {
    let results: [Result]

    enum CodingKeys: String, CodingKey {
        case results
    }
}

// MARK: - Result
struct Result: Codable {
    let geometry: Geometry
    let name: String
    let openingHours: OpeningHours?
    let photos: [Photo]?
    let rating: Double
    let vicinity: String

    enum CodingKeys: String, CodingKey {
        case geometry, name
        case openingHours = "opening_hours"
        case photos
        case rating
        case vicinity
    }
}

// MARK: - Geometry
struct Geometry: Codable {
    let location: Location
}

// MARK: - Location
struct Location: Codable {
    let lat, lng: Double
}

// MARK: - OpeningHours
struct OpeningHours: Codable {
    let openNow: Bool

    enum CodingKeys: String, CodingKey {
        case openNow = "open_now"
    }
}

// MARK: - Photo
struct Photo: Codable {
    let photoReference: String

    enum CodingKeys: String, CodingKey {
        case photoReference = "photo_reference"
    }
}

My Model:

import Foundation
struct BloodBanksModel {
    let name: String
    let photo: String
    let open_now: Bool
    let longitude: Double
    let latitude: Double
    let vincinity: String
    let rating: Double
}

My Manager class:

import Foundation

class BloodBanksManager {
    var bloodBanksArray = [BloodBanksModel]()
    
    
    //MARK: - Decoding JSON
    func performRequest(){
        if let url = URL(string: "https://maps.googleapis.com/maps/api/place/nearbysearch/json?location=XX,XX&radius=1000.0&rankby=prominence&sensor=true&key=XXXXX&keyword=blood") {
            
            let session = URLSession(configuration: .default)
            
            let task = session.dataTask(with: url) { (data, response, error) in
                if error != nil {
                    print(error!)
                    return
                }
                
                if let safeData = data {
                    self.parseJSON(bloodBankData: safeData)
                }
            }
            task.resume()
        }
    }
    
    func parseJSON(bloodBankData: Data) {
        let decoder = JSONDecoder()
        
        do {
        let decodedData = try decoder.decode(BloodBanksData.self, from: bloodBankData)
            for i in 0...decodedData.results.count - 1 {
                bloodBanksArray.append(BloodBanksModel(name: decodedData.results[i].name, photo: decodedData.results[i].photos?[0].photoReference ?? decodedData.results[0].photos![0].photoReference, open_now: decodedData.results[i].openingHours?.openNow ?? false, longitude: decodedData.results[i].geometry.location.lng, latitude: decodedData.results[i].geometry.location.lat, vincinity: decodedData.results[i].vicinity, rating: decodedData.results[i].rating))
            }
            
        } catch {
            print(error)
        }
    }
    
}

My View Controller:

   var bloodBanksManager = BloodBanksManager()
    override func viewDidLoad() {
            super.viewDidLoad()
    ...
    bloodBanksManager.performRequest()
    ...
    }
// MARK: - UICollectionViewDataSource Methods
extension LandingViewController: UICollectionViewDataSource {
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 1
    }
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 3
    }
    
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: K.BloodBanks.bloodBankCellIdentifier, for: indexPath) as! BloodBanksCell
 cell.bloodBankName.text = self.bloodBanksManager.bloodBanksArray[indexPath.row].name
        cell.bloodBankImageView.sd_setImage(with: URL(string: "https://maps.googleapis.com/maps/api/place/photo?photoreference=\(bloodBanksManager.bloodBanksArray[indexPath.row].photo)&sensor=false&maxheight=1000&maxwidth=1000&key=XXX"), placeholderImage: #imageLiteral(resourceName: "bloodbank4"))
        
        return cell
    }
    
}
mohamad
  • 89
  • 7
  • When is called `collectionView.reloadData()` Why is `numberOfItemsInSection` hard coded to `3` ? shouldn' it be linked to `bloodBanksManager.bloodBanksArray.count` instead? – Larme May 10 '21 at 15:39

2 Answers2

2

You should change your numberOfItemsInSection method to:

func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return self.bloodBanksManager.bloodBanksArray.count
}

By returning 3 you are assuming that your array is always three items long, so the exception is thrown every time your bloodBanksArray has more than 3 items.

EDIT - Code optimizations

  1. You could try to optimize your parseJSON function by decoding to an array, avoiding the for loop:
func parseJSON(bloodBankData: Data) {
    let decoder = JSONDecoder()
        
    do {
        bloodBanksArray = try decoder.decode([BloodBanksData].self, from: bloodBankData)
    } catch {
        print(error)
    }
}
  1. You could change your cellForItemAt method like this, avoiding the multiple access to the same row item.
    Also, use a global variable for the placeholderImage, since it is always the same.
    Finally, try to reduce the value for the maxheight and maxwidth parameters:
let placeholderImage = #imageLiteral(resourceName: "bloodbank4")

...

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: K.BloodBanks.bloodBankCellIdentifier, for: indexPath) as! BloodBanksCell
    let bloodBankItem = self.bloodBanksManager.bloodBanksArray[indexPath.row]
    cell.bloodBankName.text = bloodBankItem.name

    let maxImageSize = 500
    guard let imgUrl = URL(string: "https://maps.googleapis.com/maps/api/place/photo?photoreference=\(bloodBankItem.photo)&sensor=false&maxheight=\(maxImageSize)&maxwidth=\(maxImageSize)&key=XXX") else {
        return cell
    }
    cell.bloodBankImageView.sd_setImage(with: imgUrl, placeholderImage: placeholderImage)
        
    return cell
}

Other reasons for slow loading could be: 3. Slow internet connection 4. The library used for loading the image is not using lazy loading, for more information about lazy loading on UITableView/UICollectionView see this

lpizzinidev
  • 12,741
  • 2
  • 10
  • 29
  • Wow I feel so dumb now. Thank you so much man! Works perfectly. My goal was to show 3 returns only because I have a "See All" button to show everything, but now I figured out how to do it! One more thing if you don't mind me asking, is there a way to make them show up "faster"? – mohamad May 10 '21 at 16:03
  • What do you mean by "faster"? Is the list slow to show up or the images take time to render? – lpizzinidev May 10 '21 at 16:08
  • It takes like 5 seconds for the images/texts to show – mohamad May 10 '21 at 16:11
  • @mohamad I've edited my answer with some suggestions. – lpizzinidev May 10 '21 at 16:39
2
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 3
    }

Should become this:

func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return self.bloodBanksManager.bloodBanksArray.count
}

I assume 3/4ths of the time your bloodBanksArray has 3 or fewer items, but the times it doesn't it throws an error.

Neel Kumar
  • 49
  • 5