I am following this tutorial https://medium.com/#jamesrochabrun/parsing-json-response-and-save-it-in-coredata-step-by-step-fb58fc6ce16f
But before even reaching the stage of fetching , I ran the app as the tutorial suggests to get the filePath where data is stored
My app crashes and I get the error
'NSInvalidArgumentException', reason: '+entityForName: nil is not a
legal NSManagedObjectContext parameter searching for entity name
'NewsObject''
I looked around and found this iOS: Swift: Core Data: Error: +entityForName: nil is not a legal NSManagedObjectContext parameter searching for entity name
and Core Data Reading Data
They both suggest making sure manageObjectContext is not nil. However I don't how to implement that/ make sure it's not nil
I am a complete beginner in CoreData and just started with that tutorial
Here's my Code
private func createNewsEntityFrom(dictionary: [String: Any]) -> NSManagedObject? {
let context = CoreDataStack.sharedInstance.persistentContainer.viewContext
if let newsEntity = NSEntityDescription.insertNewObject(forEntityName: "NewsObject", into: context) as? NewsObject {
newsEntity.newsAuthor = dictionary["author"] as? String ?? "default"
newsEntity.newsTitle = dictionary["title"] as? String ?? "default"
let images = dictionary["image"] as? [String: AnyObject]
newsEntity.newsImageURL = images?["link"] as? String ?? "default"
return newsEntity
}
return nil
}
private func saveInCoreDataWith(array: [[String: Any]]) {
for dict in array {
_ = self.createNewsEntityFrom(dictionary: dict)
}
do {
try CoreDataStack.sharedInstance.persistentContainer.viewContext.save()
} catch let error {
print(error)
}
}
let url = "someURL"
Alamofire.request(url, method: .get , headers: headers).responseJSON { response in
switch response.result {
case .success:
let json = response.result.value as! [String:Any]
let data = json["data"] as! [[String : Any]]
self.saveInCoreDataWith(array: data)
self.nextToken = json["nextPageToken"] as? String ?? "empty"
print("Token = "+self.nextToken!)
for dic in data{
self.news.append(News(dictionary: dic))
print(self.news.count)
}
DispatchQueue.main.async {
loadingIndicator.stopAnimating()
self.tableView.reloadData()
}
case .failure: break
}
My CoreDataStack class
import UIKit
import Foundation
import CoreData
class CoreDataStack: NSObject {
static let sharedInstance = CoreDataStack()
private override init() {}
lazy var persistentContainer: NSPersistentContainer = {
/*
The persistent container for the application. This implementation
creates and returns a container, having loaded the store for the
application to it. This property is optional since there are legitimate
error conditions that could cause the creation of the store to fail.
*/
let container = NSPersistentContainer(name: "TapIn")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
/*
Typical reasons for an error here include:
* The parent directory does not exist, cannot be created, or disallows writing.
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
* The device is out of space.
* The store could not be migrated to the current model version.
Check the error message to determine what the actual problem was.
*/
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
// MARK: - Core Data Saving support
func saveContext () {
let context = persistentContainer.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
}
extension CoreDataStack {
func applicationDocumentsDirectory() {
// The directory the application uses to store the Core Data store file. This code uses a directory named "yo.BlogReaderApp" in the application's documents directory.
if let url = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).last {
print(url.absoluteString)
}
}
}
EDIT : Here's the struct News I am using to hold the data received and perform all sorts of functionality on them
//Model to hold our news
struct News {
var image : String
var title : String
var publisherIcon : String
var publisher : String
var author : String
var time : Int
var id : String
var bookmarked : Bool
var liked : Bool
init(dictionary : [String:Any])
{
let image = dictionary["image"] as? [String:Any]
self.image = image!["link"] as! String
self.title = dictionary["title"] as? String ?? ""
self.publisherIcon = dictionary["shortenedLogo"] as? String ?? ""
self.publisher = dictionary["publisher"] as? String ?? ""
self.author = dictionary["author"] as? String ?? ""
self.time = dictionary["timeToRead"] as? Int ?? 0
self.id = dictionary["_id"] as? String ?? ""
self.bookmarked = dictionary["bookmarked"] as? Bool ?? false
self.liked = dictionary["liked"] as? Bool ?? false
}
}
And in the main VC var news = [News]()
Add this lazy instantiated property in the singleton to get the non-optional context
lazy var managedObjectContext : NSManagedObjectContext = {
return self.persistentContainer.viewContext
}()
And use the modern API to insert an object
private func createNewsEntityFrom(dictionary: [String: Any]) -> NewsObject {
let context = CoreDataStack.sharedInstance.managedObjectContext
let newsEntity = NewsObject(context: context)
newsEntity.newsAuthor = dictionary["author"] as? String ?? "default"
newsEntity.newsTitle = dictionary["title"] as? String ?? "default"
let images = dictionary["image"] as? [String: Any]
newsEntity.newsImageURL = images?["link"] as? String ?? "default"
return newsEntity
}
And according to the Naming Guidelines it's highly recommended to name entity and attributes less redundant for example
private func createNews(from dictionary: [String: Any]) -> News {
let context = CoreDataStack.sharedInstance.managedObjectContext
let news = News(context: context)
news.author = dictionary["author"] as? String ?? "default"
news.title = dictionary["title"] as? String ?? "default"
let images = dictionary["image"] as? [String: Any]
news.imageURL = images?["link"] as? String ?? "default"
return news
}
By the way the applicationDocumentsDirectory function is wrong. It must be
func applicationDocumentsDirectory() -> URL {
// The directory the application uses to store the Core Data store file. This code uses a directory named "yo.BlogReaderApp" in the application's documents directory.
return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).last!
}
Edit:
To return the inserted array you have to change saveInCoreDataWith to
private func saveInCoreDataWith(array: [[String: Any]]) -> [NewsObject] {
var newsArray = [NewsObject]()
for dict in array {
newsArray.append(self.createNewsEntityFrom(dictionary: dict))
}
do {
try CoreDataStack.sharedInstance.managedObjectContext.save()
return newsArray
} catch let error {
print(error)
}
return []
}
and in the Alamofire closure replace
self.saveInCoreDataWith(array: data)
self.nextToken = json["nextPageToken"] as? String ?? "empty"
print("Token = "+self.nextToken!)
for dic in data{
self.news.append(News(dictionary: dic))
print(self.news.count)
}
with
self.news = self.saveInCoreDataWith(array: data)
self.nextToken = json["nextPageToken"] as? String ?? "empty"
print("Token = "+self.nextToken!)
print(self.news.count)
Related
I've been trying to convert the document retrieved from the Firebase's Cloud Firestore to a custom object in Swift 5. I'm following the documentation:
https://firebase.google.com/docs/firestore/query-data/get-data#custom_objects
However, Xcode shows me the error Value of type 'NSObject' has no member 'data' for the line try $0.data(as: JStoreUser.self). I've defined the struct as Codable.
The code:
func getJStoreUserFromDB() {
db = Firestore.firestore()
let user = Auth.auth().currentUser
db.collection("users").document((user?.email)!).getDocument() {
(document, error) in
let result = Result {
try document.flatMap {
try $0.data(as: JStoreUser.self)
}
}
}
}
The user struct:
public struct JStoreUser: Codable {
let fullName: String
let whatsApp: Bool
let phoneNumber: String
let email: String
let creationDate: Date?
}
The screenshot:
Does anyone know how to resolve this?
After contacting the firebase team, I found the solution I was looking for. It turns out I have to do import FirebaseFirestoreSwift explicitly instead of just doing import Firebase. The error will disappear after this. (And of course you'll need to add the pod to your podfile first:D)
You can do it as shown below:-
First create model class:-
import FirebaseFirestore
import Firebase
//#Mark:- Users model
struct CommentResponseModel {
var createdAt : Date?
var commentDescription : String?
var documentId : String?
var dictionary : [String:Any] {
return [
"createdAt": createdAt ?? "",
"commentDescription": commentDescription ?? ""
]
}
init(snapshot: QueryDocumentSnapshot) {
documentId = snapshot.documentID
var snapshotValue = snapshot.data()
createdAt = snapshotValue["createdAt"] as? Date
commentDescription = snapshotValue["commentDescription"] as? String
}
}
Then you can convert firestore document into custom object as shown below:-
func getJStoreUserFromDB() {
db = Firestore.firestore()
let user = Auth.auth().currentUser
db.collection("users").document((user?.email)!).getDocument() { (document, error) in
// Convert firestore document your custom object
let commentItem = CommentResponseModel(snapshot: document)
}
}
You need to initialize your struct and then you can extend the QueryDocumentSnapshot and QuerySnapshot like:
extension QueryDocumentSnapshot {
func toObject<T: Decodable>() throws -> T {
let jsonData = try JSONSerialization.data(withJSONObject: data(), options: [])
let object = try JSONDecoder().decode(T.self, from: jsonData)
return object
}
}
extension QuerySnapshot {
func toObject<T: Decodable>() throws -> [T] {
let objects: [T] = try documents.map({ try $0.toObject() })
return objects
}
}
Then, try to call the Firestore db by:
db.collection("users").document((user?.email)!).getDocument() { (document, error) in
guard error == nil else { return }
guard let commentItem: [CommentResponseModel] = try? document.toObject() else { return }
// then continue with your code
}
In the past, I had some issues though importing FirebaseFirestore with the package manager in my project.
So I explain about the access to FirebaseFirestore in swift.
SnapshotListener
import Foundation
import FirebaseFirestore
class BooksViewModel: ObservableObject {
#Published var books = [Book]()
private var db = Firestore.firestore()
func fetchData() {
db.collection("books").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.books = documents.map { queryDocumentSnapshot -> Book in
let data = queryDocumentSnapshot.data()
let title = data["title"] as? String ?? ""
let author = data["author"] as? String ?? ""
let numberOfPages = data["pages"] as? Int ?? 0
return Book(id: .init(), title: title, author: author, numberOfPages: numberOfPages)
}
}
}
}
using uid and getDocument function
Firestore.firestore().collection("users").document(uid).getDocument { snapshot, error in
if let error = error {
self.errorMessage = "Failed to fetch current user: \(error)"
print("Failed to fetch current user:", error)
return
}
guard let data = snapshot?.data() else {
self.errorMessage = "No data found"
return
}
let uid = data["uid"] as? String ?? ""
let email = data["email"] as? String ?? ""
let profileImageUrl = data["profileImageUrl"] as? String ?? ""
self.chatUser = ChatUser(uid: uid, email: email, profileImageUrl: profileImageUrl)
}
I am working on an iOS app (Swift) which fetches a huge amount of data (21000 records) through a web service (in chunks of 1000 records per request). And at the end of each request I need to store those 1000 records in core data. This is what I have done so far:
AppDelegate
// MARK: - Core Data stack
lazy var persistentContainer: NSPersistentContainer = {
/*
The persistent container for the application. This implementation
creates and returns a container, having loaded the store for the
application to it. This property is optional since there are legitimate
error conditions that could cause the creation of the store to fail.
*/
let container = NSPersistentContainer(name: "ABC")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
/*
Typical reasons for an error here include:
* The parent directory does not exist, cannot be created, or disallows writing.
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
* The device is out of space.
* The store could not be migrated to the current model version.
Check the error message to determine what the actual problem was.
*/
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
// MARK: - Core Data Saving support
func saveContext () {
let context = persistentContainer.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
Global Variables (At the end of AppDelegate)
let global_appDelegate = UIApplication.shared.delegate as! AppDelegate
let global_context = global_appDelegate.persistentContainer.viewContext
ViewController
func downloadMedicines( offset: Int64 )
{
let total_drugs_count = UserDefaults.standard.integer(forKey: "drug_count")
var dCount: Int64 = offset
ClassNetworkService.request_data(TagText: "Medicines", myURL: "&resource=meds", myPostParam: "dcount=\(dCount)&token=\(UserDefaults.standard.getToken())", showAlert: nil) { ( data, thread_error_title, thread_error_message ) in
DispatchQueue.main.async {
print("now its ----> M E D I C I N E S -#- \(dCount)")
if ( thread_error_title == "" )
{
if let _d_count = data["dcount"] as? Int64
{
dCount = _d_count
}
if let _data = data["data"] as? NSArray
{
for tmp_data in _data
{
if let tmp_data_dictionary = tmp_data as? NSDictionary
{
let table_medicine = Medicine(context: global_context)
table_medicine.id = Int64(tmp_data_dictionary["mID"] as! String)!
table_medicine.supplier = (tmp_data_dictionary["supplier"] as! String)
table_medicine.displayNdc = (tmp_data_dictionary["display_ndc"] as! String)
table_medicine.medispanGpi = (tmp_data_dictionary["medispan_gpi"] as! String)
table_medicine.medicationName = (tmp_data_dictionary["selldescription"] as! String)
table_medicine.genericTherapClass = (tmp_data_dictionary["generic_therap_class"] as! String)
table_medicine.ahfsTherapClass = (tmp_data_dictionary["ahfs_therap_class"] as! String)
table_medicine.keyword = (tmp_data_dictionary["keyword"] as! String)
table_medicine.memberNumber = Int64(tmp_data_dictionary["member_number"] as! String)!
table_medicine.notes = (tmp_data_dictionary["notes"] as! String)
table_medicine.pricePerUnit = Double(tmp_data_dictionary["price_per_unit"] as! String)!
table_medicine.drugOrder = Int64(tmp_data_dictionary["drug_order"] as! String)!
table_medicine.displayedStrength = (tmp_data_dictionary["displayed_strength"] as! String)
table_medicine.displayUnits = (tmp_data_dictionary["display_units"] as! String)
table_medicine.expDate = (tmp_data_dictionary["exp_date"] as! String)
table_medicine.soldUnits = (tmp_data_dictionary["sold_units"] as! String)
table_medicine.soldUnitsPlural = (tmp_data_dictionary["sold_units_p"] as! String)
table_medicine.pkgQty = (tmp_data_dictionary["pkg_qty"] as! String)
table_medicine.genericInd = (tmp_data_dictionary["generic_ind"] as! String)
table_medicine.defaultQty = Int64(tmp_data_dictionary["default_qty"] as! String)!
global_appDelegate.saveContext()
}
}
}
// download and sync more medicines here
let request_medicine = NSFetchRequest<NSFetchRequestResult>(entityName: "Medicine")
do
{
let all_medicine = try global_context.fetch(request_medicine)
if ( all_medicine.count < total_drugs_count ) // total_drugs_count
{
self.downloadMedicines( offset: dCount )
}
else
{
// syncing complete
}
}
catch
{
print (error)
}
}
}
}
}
As long as my web service is being processed my UI stands smooth but as soon as data save logic executes my UI freezes. I want to get rid of this UI freeze problem. I know it can be done by using background threads or something like this but I am still unable to figure out any solution. Any help would be greatly appreciated. Thanks
Thank you all for your suggestions. I managed to solve this issue. Posting my code below in case someone else needs it.
// Creates a task with a new background context created on the fly
global_appDelegate.persistentContainer.performBackgroundTask { (context) in
for tmp_data in _data
{
if let tmp_data_dictionary = tmp_data as? NSDictionary
{
let table_medicine = Medicine(context: context)
table_medicine.id = Int64(tmp_data_dictionary["mID"] as! String)!
table_medicine.supplier = (tmp_data_dictionary["supplier"] as! String)
table_medicine.displayNdc = (tmp_data_dictionary["display_ndc"] as! String)
table_medicine.medispanGpi = (tmp_data_dictionary["medispan_gpi"] as! String)
table_medicine.medicationName = (tmp_data_dictionary["selldescription"] as! String)
table_medicine.genericTherapClass = (tmp_data_dictionary["generic_therap_class"] as! String)
table_medicine.ahfsTherapClass = (tmp_data_dictionary["ahfs_therap_class"] as! String)
table_medicine.keyword = (tmp_data_dictionary["keyword"] as! String)
table_medicine.memberNumber = Int64(tmp_data_dictionary["member_number"] as! String)!
table_medicine.notes = (tmp_data_dictionary["notes"] as! String)
table_medicine.pricePerUnit = Double(tmp_data_dictionary["price_per_unit"] as! String)!
table_medicine.drugOrder = Int64(tmp_data_dictionary["drug_order"] as! String)!
table_medicine.displayedStrength = (tmp_data_dictionary["displayed_strength"] as! String)
table_medicine.displayUnits = (tmp_data_dictionary["display_units"] as! String)
table_medicine.expDate = (tmp_data_dictionary["exp_date"] as! String)
table_medicine.soldUnits = (tmp_data_dictionary["sold_units"] as! String)
table_medicine.soldUnitsPlural = (tmp_data_dictionary["sold_units_p"] as! String)
table_medicine.pkgQty = (tmp_data_dictionary["pkg_qty"] as! String)
table_medicine.genericInd = (tmp_data_dictionary["generic_ind"] as! String)
table_medicine.defaultQty = Int64(tmp_data_dictionary["default_qty"] as! String)!
if let tmp_insurances = tmp_data_dictionary["insurance"] as? NSArray
{
do
{
let jsonData = try JSONSerialization.data(withJSONObject: tmp_insurances, options: JSONSerialization.WritingOptions.prettyPrinted)
if let JSONString = String(data: jsonData, encoding: String.Encoding.utf8)
{
table_medicine.insurance = JSONString
}
}
catch
{
print(error)
}
}
//global_appDelegate.saveContext()
do {
try context.save()
} catch {
fatalError("Failure to save context: \(error)")
}
}
}
}
I try to implement a simple shopping list swift application for iOS as a personal project. I did follow a guide for iOS on youtube.
My question is how do I parse the Item object from firebase to my ShoppingListItem swift object? If I execute the following code, it doesn't show any error message but it does not show any results either. If I uncomment all "items" lines, it shows the expected results without the item information.
Here is a screenshot from the firebase console of my firebase firestore structure / example object
Thanks in advance!
ShoppingListItem.swift
import Foundation
import FirebaseFirestore
protocol DocumentSerializable {
init?(dictionary: [String: Any])
}
struct ShoppingListItem {
var shoppingItemID: String
var priority: Int
var quantity: Int
var item: Item
var dictionary: [String: Any] {
return [
"shoppingItemID": shoppingItemID,
"priority": priority,
"quantity": quantity,
"item": item,
]
}
}
extension ShoppingListItem: DocumentSerializable {
init?(dictionary: [String : Any]) {
guard let shoppingItemID = dictionary["shoppingItemID"] as? String,
let priority = dictionary["priority"] as? Int,
let quantity = dictionary["quantity"] as? Int,
let item = dictionary["item"] as? Item
else { return nil }
self.init(shoppingItemID: shoppingItemID, priority: priority, quantity: quantity, item: item)
}
}
struct Item {
var itemID: String
var lastPurchase: String
var name: String
var note: String
var picturePath: String
var dictionary: [String: Any] {
return [
"itemID": itemID,
"lastPurchase": lastPurchase,
"name": name,
"note": note,
"picturePath": picturePath,
]
}
}
extension Item: DocumentSerializable {
init?(dictionary: [String : Any]) {
guard let itemID = dictionary["itemID"] as? String,
let lastPurchase = dictionary["lastPurchase"] as? String,
let name = dictionary["name"] as? String,
let note = dictionary["note"] as? String,
let picturePath = dictionary["picturePath"] as? String else { return nil }
self.init(itemID: itemID, lastPurchase: lastPurchase, name: name, note: note, picturePath: picturePath)
}
}
Get Data call in TableViewController.swift
db.collection("shoppingList").getDocuments(){
querySnapshot, error in
if let error = error {
print("error loading documents \(error.localizedDescription)")
} else{
self.shoppingArray = querySnapshot!.documents.flatMap({ShoppingListItem(dictionary: $0.data())})
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
I used the Codable protocol.
I used this as an extension to the Encodable Protocol:
extension Encodable {
/// Returns a JSON dictionary, with choice of minimal information
func getDictionary() -> [String: Any]? {
let encoder = JSONEncoder()
guard let data = try? encoder.encode(self) else { return nil }
return (try? JSONSerialization.jsonObject(with: data, options: .allowFragments)).flatMap { $0 as? [String: Any]
}
}
}
Then I use this to decode:
extension Decodable {
/// Initialize from JSON Dictionary. Return nil on failure
init?(dictionary value: [String:Any]){
guard JSONSerialization.isValidJSONObject(value) else { return nil }
guard let jsonData = try? JSONSerialization.data(withJSONObject: value, options: []) else { return nil }
guard let newValue = try? JSONDecoder().decode(Self.self, from: jsonData) else { return nil }
self = newValue
}
}
Make your two structs conform to Codable (Item first, then ShoppingListItem). Of course, this may not work for the existing data stored in Firestore. I would first put data into Firestore via the getDictionary() (in a new collection), then try to read it back into your tableView.
You may also want to print the actual error when trying to Decode your data, this will greatly help you pinpoint the data error if there's any.
extension Decodable {
/// Initialize from JSON Dictionary. Return nil on failure
init?(dictionary value: [String:Any]) {
guard JSONSerialization.isValidJSONObject(value) else {
return nil
}
do {
let jsonData = try JSONSerialization.data(withJSONObject: value, options: [])
let newValue = try JSONDecoder().decode(Self.self, from: jsonData)
self = newValue
}
catch {
log.error("failed to serialize data: \(error)")
return nil
}
}
}
So I have two models, one called AppCategory and App. I've created a function within the AppCategory class that makes an API call that brings me back all categories successfully and within those categories there is an array of dictionaries of apps which I get back in form of dictionaries but i'm not sure on how to set it as an App object.
Here is my AppCategory class:
import UIKit
class AppCategory {
var name: String?
var apps = [App]()
var type: String?
init(dictionary: [String: Any]) {
name = dictionary["name"] as? String
apps = dictionary["apps"] as! [App]
type = dictionary["type"] as? String
}
static func fetchFeaturedApps(completion: #escaping ([AppCategory]) -> Void) {
guard let url = URL(string: "https://api.letsbuildthatapp.com/appstore/featured") else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
if error != nil {
print(error!.localizedDescription)
return
}
guard let data = data else { return }
do {
let json = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as! [String: Any]
var appCategories = [AppCategory]()
for dict in json["categories"] as! [[String: Any]] {
let appCategory = AppCategory(dictionary: dict)
appCategories.append(appCategory)
if dict.index(forKey: "apps") != nil {
}
}
print(appCategories)
DispatchQueue.main.async {
completion(appCategories)
}
} catch {
print("Error in JSON Serialization")
}
}.resume()
}
}
Here is the App model class:
import Foundation
class App {
var id: Int?
var name: String?
var category: String?
var price: Double?
var imageName: String?
init(dictionary: [String: Any]) {
id = dictionary["Id"] as? Int
name = dictionary["Name"] as? String
category = dictionary["Category"] as? String
price = dictionary["Price"] as? Double
imageName = dictionary["ImageName"] as? String
}
}
I've done research and I could make these classes of type NSObject and use the setValue(forKey:) but I don't really want to do that. I've gotten as far as what's highlighted in the AppCategory class by saying if dict.index(forKey: "apps") != nil... Don't know if i'm on the right track but maybe someone can help me with this.
I am new to iOS development and been trying to jump into swift straight away. I am trying to work with APIs and trying to learn myself. I've build this test Collection view along with the model to get the data however when I run the app I get a crash. Been trying to find a solution with no luck.
I've seen few that have the same crash however mostly due to a xib file which I am not using. I am building the app solely in code.
AlbumId
import UIKit
class AlbumId: NSObject {
var albumId: NSNumber?
static func fetchAlbums() {
let urlString = "https://jsonplaceholder.typicode.com/photos"
let url = URL(string: urlString)
URLSession.shared.dataTask(with: url!) { (data, response, error) in
if error != nil {
print(error ?? "")
return
}
do {
let json = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers)
var albums = [Album]()
for dict in json as! [Any] {
let album = Album(dictionary: dict as! [String: Any])
album.setValuesForKeys(dict as! [String : Any])
albums.append(album)
}
} catch let err {
print(err)
}
}.resume()
}
}
Album
class Album: NSObject {
var id: NSNumber?
var title: String?
var url: String?
var thumbnailUrl: String?
init(dictionary: [String: Any]) {
super.init()
id = dictionary["id"] as? NSNumber
title = dictionary["title"] as? String
url = dictionary["url"] as? String
thumbnailUrl = dictionary["thumbnailUrl"] as? String
}
}
Your class Album does not have a property called albumId which means that it is not KVC compliant for that key.
It seems your JSON response has a key "albumId", but since your class is not KVC-compliant (it doesn't have an "albumId" property) using setValuesForKeys fails because setValuesForKeys requires that the instance must be KVC compliant for all of the keys in the dictionary.
Without a little knowledge regarding the JSON response, we can only make recommendations based on assumptions.
Your options are:
Change the property "id" to "albumId" on class Album
Change your API so the JSON key is simply "id"
Override setValueForKey and redirect "albumId" to your "id" property.
The error occurs because the model does not contain the property albumId.
Calling the KVC method setValuesForKeys is redundant anyway since you are initializing the object from the dictionary. There are only a few rare cases in Swift where KVC is useful. This is none of them. And inheritance from NSObject is actually not needed either.
The received JSON has id and albumId keys, so add the latter to the model and use Int rather than NSNumber. This code uses non-optional constants (let) with default values empty string / 0
class Album {
let albumId : Int
let id: Int
let title: String
let url: String
let thumbnailUrl: String
init(dictionary: [String: Any]) {
albumId = dictionary["albumId"] as? Int ?? 0
id = dictionary["id"] as? Int ?? 0
title = dictionary["title"] as? String ?? ""
url = dictionary["url"] as? String ?? ""
thumbnailUrl = dictionary["thumbnailUrl"] as? String ?? ""
}
}
Now populate the albums array, as always .mutableContainers is completely meaningless in Swift
if let json = try JSONSerialization.jsonObject(with: data!) as? [[String: Any]] else { return }
var albums = [Album]()
for dict in json {
let album = Album(dictionary: dict)
albums.append(album)
}
or in a swiftier way
var albums = json.map { Album(dictionary: $0) }