An error happened when fetching API and decoding JSON file

I am going to decode a Magic cards database from a JSON file fetched from this API (here). however, an error appears when I try to plot some data in the canvas with SwiftUI.

I thought I might do something worry when I called the FetchdAndDecodedMtgData model at the first time. But I got confused when I set some print() debugger in my FetchdAndDecodedMtgData model because these debuggers don't print anything in the debug monitor.

I don't know how to solve it. If the FetchdAndDecodedMtgData model is correct, then how to call the decodedData in the ContentView?
Any help is appreciated!

The FetchdAndDecodedMtgData model

struct FetchdAndDecodedMtgData {
    let urlString: String
    var url: URL?
    
    init(urlString: String) {
        self.urlString = urlString
        url = URL(string: urlString) ?? nil
    }
    
    func fetchdAndDecodeApiData(completion: @escaping (MTGData?, Error?) -> ()) {
        let task = URLSession.shared.dataTask(with: url!) { data, response, error in
            
            if let error = error {
                completion(nil, error)
                print("! Error")
                return
            }
            
            //check response status code etc if needed 
            
            //parse data
            guard let data = data else {
                completion(nil, NameError.invalidData)
                print("! Error")
                return
            }
            
            do {
                let decoder = JSONDecoder()
                let decodedData = try decoder.decode(MTGData.self, from: data)
                print(decodedData)
                
                DispatchQueue.main.async {
                    completion(decodedData, nil)
                }
            } catch {
                completion(nil, error)
            }
        }
        task.resume()
    }
}

The ContentView:

struct ContentView: View {
    static let urlString: String = "https://mtgjson.com/api/v5/YNEO.json"
    let fetchdAndDecodedModel: FetchdAndDecodedMtgData = FetchdAndDecodedMtgData(urlString: urlString)
    @State var mtgData: MTGData!
    
    var body: some View {
        VStack {
            Text("API retrive date: \(mtgData.meta.date)")
                .padding()
            Text("API retrive version: \(mtgData.meta.version)")
                .padding()
            Divider()
            Text("Card Number: \(mtgData.data.baseSetSize)")
        }
        .onAppear() {
            fetchdAndDecodedModel.fetchdAndDecodeApiData { (result, error) in
                self.mtgData = result!
            }
        }
    }
}

If you put breakpoints on the two lines you'll see that what's happening here is that the line:

Text("API retrive date: \(mtgData.meta.date)")

is executed before the line:

self.mtgData = result!

Easy fix would be this:

@State var mtgData: MTGData = MTGData(... some empty data here)

The view will show that placeholder data first, then when the line "self.mtgData = result!" is executed the view will be updated.

You may want to decouple the model from the view: put it in a separate observable object, then you'll be able using it from more than one view, view's onAppear won't necessarily trigger fetch if that was already done previously, view will focus on view only aspects:

struct MyView: View {
    @ObservedObject var model: Model
    
    var body: some View {
        VStack {
            Text(model.xxxx)
        }
        .onAppear() {
            model.onAppear()
        }
        .onDisappear {
            model.onDisappear()
        }
    }
}

Hi Tera,

Thanks for passing by and replying.

If I go with your second solution, by decoupling the model from the view. In this case, I could call the fetchdAndDecodeApiData function in the model like:

@ObservableObject var model: Model
model. fetchdAndDecodeApiData

However, how I can get the mtgData from the completion handler in the fetchdAndDecodeApiData function? The "fetchdAndDecodeApiData" function doesn't have return syntax but saves the fetching result in a closure argument: "completion: (MTGData?, Error?)". So, in the view model, is there a way to fetch the mtgdata from the completion handler?

The view model (or "the model" if you don't have the "view model" / "data model" separation) will store the fetched data into a property, and if that property is marked "published" and the model is "observed" by the view - the view will update.

class Model: ObservableObject {
	@Published var xxxx: String?
	
	func onAppear() {
		if xxxx == nil {
			fetch { data, response, error in
				let decoded = decode data
				xxxx = decoded     // assign on main thread
			}
		}
	}
	
	func onDisappear() {}
}

Hi Tera,

There is another convenient way instead of using a completion handler to fetch and decode JSON is using async/await which is introduced in WWDC21.
Here is the linkWWDC 2021 async/await

Indeed.

Full minimal example (untested).
struct SomeData: Decodable {
    let string: String
}

@available(iOS 15, *)
class Model: ObservableObject {
    @Published var someVariable: String?
    
    func onAppear() async {
        guard someVariable == nil else { return }
        let url = ...
        guard let data = try? await URLSession.shared.data(from: url, delegate: nil).0 else { return }
        guard let someData = try? JSONDecoder().decode(SomeData.self, from: data) else { return }
        dispatchPrecondition(condition: .onQueue(.main))
        someVariable = someData.string
    }
}

@available(iOS 15, *)
struct MyView: View {
    @ObservedObject var model: Model
    var body: some View {
        Text(model.someVariable ?? "n/a")
            .onAppear {
                Task {
                    await model.onAppear()
                }
            }
    }
}
1 Like