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
    }
    
}
1
  • When is called collectionView.reloadData() Why is numberOfItemsInSection hard coded to 3 ? shouldn' it be linked to bloodBanksManager.bloodBanksArray.count instead? Commented May 10, 2021 at 15:39

2 Answers 2

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

Sign up to request clarification or add additional context in comments.

4 Comments

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"?
What do you mean by "faster"? Is the list slow to show up or the images take time to render?
It takes like 5 seconds for the images/texts to show
@mohamad I've edited my answer with some suggestions.
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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.