Code crashing due to error: Can't remove last element from an empty collection

Viewed 595

I am trying to chart some data, the following code worked initially, and have got it to work sometimes, but now it seems to mostly crash, I thought that the if let data = data line in the json function would mean it would only run once it received the data. I have a feeling that maybe the function is unwrapping the data before it fully loads but I am not sure.

import SwiftUI
import SwiftUICharts

struct CoinGeckoBSVMarketChart: View {
   // @State var jsonData = [MarketChart]()
    @State var priceArray = [Double]()
    @State var volumeArray: [Double] = []
    
    private let last24hpricesURL: String = "https://api.coingecko.com/api/v3/coins/bitcoin-cash-sv/market_chart?vs_currency=usd&days=1"
    
    var body: some View {
        ZStack {
            VStack {
                LineView(data: priceArray)
                    .padding()
                
                Text("Reset")
                    .onTapGesture {
                        // will add a tap to refresh data by resetting the array to empty then retuning the getJSON 
                    }
            }
        }
        .onAppear {
            getJSON(urlString: last24hpricesURL) { (data: MarketChart?) in
                if let data = data {
                    for item in data.prices {
                        priceArray.append(item.last!)  // It worked the first time and still on occasion but error prone, and works in playground
                    }
                }
            }
        }
    }
}

struct MarketChart: Decodable {
    let prices: [[Double]]
    let market_caps: [[Double]]
    let total_volumes: [[Double]]
}

struct CoinGeckoBSVMarketChart_Previews: PreviewProvider {
    static var previews: some View {
        CoinGeckoBSVMarketChart()
            .preferredColorScheme(.dark)
    }
}

here is the getJSON for reference if needed :

func getJSON<T: Decodable>(urlString: String, completion: @escaping (T?) -> Void) {
    guard let url = URL(string: urlString) else {
        return
    }
    let request = URLRequest(url: url)
    URLSession.shared.dataTask(with: request) { (data, response, error) in
        if let error = error {
            print(error.localizedDescription)
            completion(nil)
            return
        }
        guard let data = data else {
            completion(nil)
            return
        }
        
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .secondsSince1970
        guard let decodedData = try? decoder.decode(T.self, from: data) else {
            completion(nil)
            return
        }
        completion(decodedData)
    }.resume()
}

in each parameter is a double with the timestamp and value, now I am just trying to revive the value. any help would be fantastic thank you

2 Answers

I was experiencing the exact same issue, and the fix was to avoid appending to the array marked with @State inside the for loop. I can only assume doing so causes unwanted behavior because of how everything observing the state gets notified with every modification of the array. So what fixed it for me is doing this:

var tempPriceArray = [Double]()
for item in data.prices {
    tempPriceArray.append(item.last!)  
}
priceArray = tempPriceArray

You need to safely add it in case there is an empty element inside let prices: [[Double]]

for item in data.prices {
   if let res = item.last {
      priceArray.append(res) 
   }
}
Related