BLACK FRIDAY SALE: Save big on all my Swift books and bundles! >>

SOLVED: Advice needed on how to handle API responses with NSManagedObjects

Forums > SwiftUI

Hi,

I am in need of some help on how to interact with an API that provides a response as a dictionary which looks like this... (DictionaryResponseSample.json)

{
  "success": true
  "payload": [
    {
      "facility_id": 1234,
      "facility_name": "facility_1"
    },
    {
      "facility_id": 1235
      "facility_name": "facility_2"
    }
  ]
}    

I have no problem directly importing into Core Data when the response is an array as in... (ArrayResponseSample.json)

[
  {
    "facility_id": 1234,
    "facility_name": "facility_1"
  },
  {
    "facility_id": 1235
    "facility_name": "facility_2"
  }
]   

The code driving this is from Donny Wals Core Data book

From DataImporter.swift

public static var sampleData: Data {
    let url = Bundle(for: Self.self).url(forResource: "ArrayResponseSample", withExtension: "json")
    return try! Data(contentsOf: url!)
}
public func importData<T: Collection & Decodable>(_ data: Data, as model: T.Type) where T.Element: NSManagedObject {
    self._importData(data, as: model)
}

private func _importData<T: Decodable>(_ data: Data, as model: T.Type) {
    context.perform { [unowned self] in
        let decoder = JSONDecoder()
        decoder.userInfo[.managedObjectContext] = self.context

        do {
            _ = try decoder.decode(model, from: data)
            try self.context.save()
            self.context.reset()
        }
        catch {
            if self.context.hasChanges {
                self.context.rollback()
            }
            print("Failed to insert models")
            print(error)
        }
    }
}

Called in ContentView.swift...

var body: some View {
VStack {
    Button("Import Data") {
        let data = DataImporter.sampleData
        importer.importData(data, as: [Facility].self)
    }
    VStack {
        List(facilities, id: \.facility_id) { (facility: Facility) in 
                Text("\(facility.facility_name)")
        }
    }
}
.padding()
}

I'd like to keep the response metadata (success) in the API so I can confirm the success of a response. I could omit 'success' on the successul API call and use Swift's error handling to determine if the response responded with a success and only return the failure metadata on a failed API call.

It'd be great to just 'slice' off the paylaod array as I could resuse an API response class, but I cannot figure out how to do that

I believe that I am trying to do is very similar to Paul's Whitehouse project here: https://www.hackingwithswift.com/read/7/3/parsing-json-using-the-codable-protocol with the difference being using NSManagedObject classes instead of just Codable classes.

This would be a big breakthrough for me so any help would be greatly appreciated. Thanks in advance!

   

Instead of using Donny Wals' generic importer , I suggest you try defining this struct and decoding it:

struct MyResponseType: Decodable {
      let success: Bool
      let payload: [Facility]
}

   

I figured out how to create a class for API responses that contains a generic. The allows me to receive API responses from a single class regardless of the type of data being returned, as long as I know what type is being returned. It looks like this....

ApiHandler.swift

class ApiHandler: Decodable {

    func sendRequest<T: Decodable>(for: T.Type = T.self, url: URL) async -> ApiResponse<T>? {
        do {
            let (data, _) = try await URLSession.shared.data(from: url)
            let decoded = try JSONDecoder().decode(ApiResponse<T>.self, from: data)
            return decoded

        }
        catch {
            print("Unhandled Error")
            print(error)
            return nil
        }
    }
}

ApiResponse.swift

class ApiResponse<T: Decodable>: Decodable {

    let success: Bool
    let response: String
    let responseDescription: String
    let dataArray: [T]?

    enum CodingKeys: CodingKey {
        case success
        case response
        case responseDescription
        case dataArray
    }

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.success = try container.decode(Bool.self, forKey: .success)
        self.response = try container.decode(String.self, forKey: .response)
        self.responseDescription = try container.decode(String.self, forKey: .responseDescription)
        self.dataArray = try container.decodeIfPresent([T].self, forKey: .dataArray)
    }
}

And it can be called like this...

let api = ApiHandler()
let url = URL(string: "http://someapiurl.com/thing?param1=abc&param2=xyz")!
let response = await api.sendRequest(for: CustomObject.self, url: url)

I pulled a lot of this from: https://stackoverflow.com/questions/48657076/return-a-generic-swift-object-from-a-rest-api-client

It shouldn't be difficult to extend to NSManagedObjects

   

Hacking with Swift is sponsored by RevenueCat

SPONSORED In-app subscriptions are a pain to implement, hard to test, and full of edge cases. RevenueCat makes it straightforward and reliable so you can get back to building your app. Oh, and it's free if your app makes less than $10k/mo.

Learn more

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.