jsonswiftloopsswiftui

Decoding JSON data SwiftUI


I am attempting to decode JSON data in SwiftUI however have run into an issue I can't seem to solve. I am able to extract the data for one ticker symbol (it presents a bunch of information about a specific stock but you have to specify the ticker symbol), but would like to present multiple ticker symbols to the user. So far I have been unsuccessful in doing so.

The logic I have been trying to use to do this has been to just run a loop to extract data for each ticker symbol I'm after, however, the results are random as to what it decodes & displays (it sometimes displays data for 3 symbols, sometimes all, sometimes none). The code I am using is below.

Note: "stockArray" is the ticker symbols I am trying to extract. "allShares" is the model I have been using to decode the JSON data.

class ShareData {

@Published var allShares: [Stock] = []
var shareSubscription: AnyCancellable?
var stockArray: [String] = ["TSLA", "AAPL", "AMZN", "MSFT", "DELL"]

init() {
    for values in stockArray {
        getShares(values: values)
    }   
}

private func getShares(values: String) {
    
        guard let url = URL(string: "https://website.com")
        else { return }
   
        shareSubscription = URLSession.shared.dataTaskPublisher(for: url)
            .subscribe(on: DispatchQueue.global(qos: .default))
            .tryMap { (output) -> Data in
                
                guard let response = output.response as? HTTPURLResponse,
                      response.statusCode >= 200 && response.statusCode < 300 else {
                    throw URLError(.badServerResponse)
                   
                }
                return output.data
                
            }
            .receive(on: DispatchQueue.main)
            .decode(type: [Stock].self, decoder: JSONDecoder())
            .sink { (completion) in
                switch completion {
                case .finished:
                    break
                case.failure(let error):
                    print(String(describing: error))
                }
            } receiveValue: { [weak self] (returnedShares) in
          
                self?.allShares.append(contentsOf: returnedShares)
                self?.shareSubscription?.cancel()
    }
}
}

I have determined the loop runs for each element in the stockArray in every part of the code except for when it reaches the .sink section. Any guidance on this issue would be greatly appreciated as I have been struggling with this issue for the past couple weeks!

Edit additional code (this is causing the error "Cannot convert return expression of type '[Stock]' to return type 'Stock?'", however, having it as just Stock.self results in the debug error "typeMismatch(Swift.Dictionary<Swift.String, Any>, Swift.DecodingError.Context(codingPath: [], debugDescription: "Expected to decode Dictionary<String, Any> but found an array instead.", underlyingError: nil))"):

func getStockData(for theSymbol: String?) async -> Stock? {
        if let symbol = theSymbol,
           let url = URL(string: "https://www.website.com\(symbol)?apikey=") {
            do {
                let (data, _) = try await URLSession.shared.data(from: url)
                    
                return try JSONDecoder().decode([Stock].self, from: data)
                
            
                    
            }
            catch { print(error) }
        }
        return nil
    }
}

Stock Strut:

struct Stock: Codable, Identifiable {
let id = UUID()
let symbol: String
let price: Double
let volAvg, mktCap: Int?
let lastDiv: Double?
let changes: Double
let companyName, currency: String?
let exchange, exchangeShortName, industry: String?
let website: String?
let description, ceo, sector, country: String?
let image: String
let currentHoldings: Double?

enum CodingKeys: String, CodingKey {
    case symbol
    case price
    case volAvg, mktCap
    case lastDiv, changes
    case companyName, currency, exchange, exchangeShortName, industry
    case website, description, ceo, sector, country, image
    case currentHoldings
   }
}

Data output:

[
   {
     "symbol": "AAPL",
     "price": 181.99,
     "volAvg": 55933106,
     "mktCap": 2862466188708,
     "lastDiv": 0.96,
     "changes": -9.18,
     "companyName": "Apple Inc.",
     "currency": "USD",
     "exchange": "NASDAQ Global Select",
     "exchangeShortName": "NASDAQ",
     "industry": "Consumer Electronics",
     "website": "https://www.apple.com",
     "description": "Apple Inc. designs, manufactures, and markets smartphones, personal computers, tablets, wearables, and accessories worldwide. It also sells various related services. In addition, the company offers iPhone, a line of smartphones; Mac, a line of personal computers; iPad, a line of multi-purpose tablets; AirPods Max, an over-ear wireless headphone; and wearables, home, and accessories comprising AirPods, Apple TV, Apple Watch, Beats products, HomePod, and iPod touch. Further, it provides AppleCare support services; cloud services store services; and operates various platforms, including the App Store that allow customers to discover and download applications and digital content, such as books, music, video, games, and podcasts. Additionally, the company offers various services, such as Apple Arcade, a game subscription service; Apple Music, which offers users a curated listening experience with on-demand radio stations; Apple News+, a subscription news and magazine service; Apple TV+, which offers exclusive original content; Apple Card, a co-branded credit card; and Apple Pay, a cashless payment service, as well as licenses its intellectual property. The company serves consumers, and small and mid-sized businesses; and the education, enterprise, and government markets. It distributes third-party applications for its products through the App Store. The company also sells its products through its retail and online stores, and direct sales force; and third-party cellular network carriers, wholesalers, retailers, and resellers. Apple Inc. was incorporated in 1977 and is headquartered in Cupertino, California.",
     "ceo": "Mr. Timothy D. Cook",
     "sector": "Technology",
     "country": "US",
     "image": "",
   }
 ]

Solution

  • You could try this more modern approach, using Swift async/await concurrency framework.

    Here is an example code to fetch all stocks in stockArray concurrently from an api and displaying it in a View. It uses withTaskGroup to get all Stock data in parallel.

    You will have to adjust the code (especially the url and the Stock struct) to suit your api and data model.

    @MainActor
    class ShareData: ObservableObject {
        
        @Published var allShares: [Stock] = []
        
        var stockArray: [String] = ["TSLA", "AAPL", "AMZN", "MSFT", "DELL", "IBM"]
        let apikey = "demo"  // <-- very limited
        
        init() {
            Task {
                await fetchAllShares()
            }
        }
        
        func fetchAllShares() async {
            // get all stocks concurrently
            await withTaskGroup(of: (Stock?).self) { group -> Void in
                stockArray.forEach { symbol in
                    group.addTask {
                        await (self.getStockData(for: symbol))
                    }
                }
                for await value in group {
                    if let stock = value {
                        allShares.append(stock)
                    }
                }
            }
        }
        
        // fetch one Stock for one symbol
        func getStockData(for theSymbol: String?) async -> Stock? {
            if let symbol = theSymbol,
               let url = URL(string: "https://www.alphavantage.co/query?function=TIME_SERIES_DAILY&symbol=\(symbol)&outputsize=full&apikey=\(apikey)") {
                do {
                    let (data, _) = try await URLSession.shared.data(from: url)
                    return try JSONDecoder().decode(Stock.self, from: data)
                }
                catch { print(error) }
            }
            return nil
        }
        
    }
    
    struct ContentView: View {
        @StateObject var sharesModel = ShareData()
        
        var body: some View {
            List(sharesModel.allShares) { stock in
                HStack (spacing: 20) {
                    Text(stock.metaData?.symbol ?? "not available")
                    Text(stock.latestClose).foregroundColor(.red)
                }
            }
        }
    }
    
    struct Stock: Identifiable, Codable {
        let id = UUID()
        var metaData: MetaData?
        var timeSeriesDaily: [String: TimeSeriesDaily]?
        
        var latestClose: String {
            if let timeseries = timeSeriesDaily {
                guard let mostRecentDate = timeseries.keys.sorted(by: >).first else { return "" }
                return timeseries[mostRecentDate]!.close
            } else {
                return ""
            }
        }
        
        private enum CodingKeys: String, CodingKey {
            case metaData = "Meta Data"
            case timeSeriesDaily = "Time Series (Daily)"
        }
    }
    
    struct MetaData: Codable {
        let information: String
        let symbol: String
        let lastRefreshed: String
        let outputSize: String
        let timeZone: String
        
        private enum CodingKeys: String, CodingKey {
            case information = "1. Information"
            case symbol = "2. Symbol"
            case lastRefreshed = "3. Last Refreshed"
            case outputSize = "4. Output Size"
            case timeZone = "5. Time Zone"
        }
    }
    
    struct TimeSeriesDaily: Codable {
        var open: String
        var high: String
        var low: String
        var close: String
        var volume: String
        
        private enum CodingKeys: String, CodingKey {
            case open = "1. open"
            case high = "2. high"
            case low = "3. low"
            case close = "4. close"
            case volume = "5. volume"
        }
    }
    

    EDIT-1:

    if you get an array [Stock] from the server, then try this code, assuming your url is correct.

        func getStockData(for theSymbol: String?) async -> Stock? {
            if let symbol = theSymbol,
               let url = URL(string: "https://www.website.com/\(symbol)?apikey=") {
                do {
                    let (data, _) = try await URLSession.shared.data(from: url)
       print("---> data: \n \(String(data: data, encoding: .utf8) as AnyObject) \n")
                    let results = try JSONDecoder().decode([Stock].self, from: data)
                    if let stock = results.first {
                        return stock
                    }
                }
                catch { print(error) }
            }
            return nil
        }